天天看點

Spring @Conditional注解 詳細講解及示例

前言:

@Conditional是Spring4新提供的注解,它的作用是按照一定的條件進行判斷,滿足條件給容器注冊bean。

@Conditional的定義:

//此注解可以标注在類和方法上

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) 
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}
           

從代碼中可以看到,需要傳入一個Class數組,并且需要繼承Condition接口:

public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}
           

Condition是個接口,需要實作matches方法,傳回true則注入bean,false則不注入。

示例:

首先,建立Person類:

public class Person {
 
    private String name;
    private Integer age;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Integer getAge() {
        return age;
    }
 
    public void setAge(Integer age) {
        this.age = age;
    }
 
    public Person(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}
           

建立BeanConfig類,用于配置兩個Person執行個體并注入,一個是比爾蓋茨,一個是林納斯。

@Configuration
public class BeanConfig {
 
    @Bean(name = "bill")
    public Person person1(){
        return new Person("Bill Gates",62);
    }
 
    @Bean("linus")
    public Person person2(){
        return new Person("Linus",48);
    }
}
           

接着寫一個測試類進行驗證這兩個Bean是否注入成功。

public class ConditionalTest {
 
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanConfig.class);
 
    @Test
    public void test1(){
        Map<String, Person> map = applicationContext.getBeansOfType(Person.class);
        System.out.println(map);
    }
}
           

 運作,輸出結果是這樣的,兩個Person執行個體被注入進容器。

這是一個簡單的例子,現在問題來了,如果我想根據目前作業系統來注入Person執行個體,windows下注入bill,linux下注入linus,怎麼實作呢?

這就需要我們用到@Conditional注解了,前言中提到,需要實作Condition接口,并重寫方法來自定義match規則。

首先,建立一個WindowsCondition類:

public class WindowsCondition implements Condition {
 
    /**
     * @param conditionContext:判斷條件能使用的上下文環境
     * @param annotatedTypeMetadata:注解所在位置的注釋資訊
     * */
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //擷取ioc使用的beanFactory
        ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
        //擷取類加載器
        ClassLoader classLoader = conditionContext.getClassLoader();
        //擷取目前環境資訊
        Environment environment = conditionContext.getEnvironment();
        //擷取bean定義的注冊類
        BeanDefinitionRegistry registry = conditionContext.getRegistry();
 
        //獲得目前系統名
        String property = environment.getProperty("os.name");
        //包含Windows則說明是windows系統,傳回true
        if (property.contains("Windows")){
            return true;
        }
        return false;
    }
}
           

matches方法的兩個參數的意思在注釋中講述了,值得一提的是,conditionContext提供了多種方法,友善擷取各種資訊,也是SpringBoot中 @ConditonalOnXX注解多樣擴充的基礎。

接着,建立LinuxCondition類:

public class LinuxCondition implements Condition {
 
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
 
        Environment environment = conditionContext.getEnvironment();
 
        String property = environment.getProperty("os.name");
        if (property.contains("Linux")){
            return true;
        }
        return false;
    }
}
           

接着就是使用這兩個類了,因為此注解可以标注在方法上和類上,是以分開測試:

标注在方法上:

修改BeanConfig:

@Configuration
public class BeanConfig {
 
    //隻有一個類時,大括号可以省略
    //如果WindowsCondition的實作方法傳回true,則注入這個bean    
    @Conditional({WindowsCondition.class})
    @Bean(name = "bill")
    public Person person1(){
        return new Person("Bill Gates",62);
    }
 
    //如果LinuxCondition的實作方法傳回true,則注入這個bean
    @Conditional({LinuxCondition.class})
    @Bean("linus")
    public Person person2(){
        return new Person("Linus",48);
    }
}
           

修改測試方法,使其可以列印目前系統名:

    @Test
    public void test1(){
        String osName = applicationContext.getEnvironment().getProperty("os.name");
        System.out.println("目前系統為:" + osName);
        Map<String, Person> map = applicationContext.getBeansOfType(Person.class);
        System.out.println(map);
    }
           

運作結果如下:

我是運作在windows上的是以隻注入了bill,嗯,沒毛病。

接着實驗linux下的情況,不能運作在linux下,但可以修改運作時參數:

修改後啟動測試方法:

 一個方法隻能注入一個bean執行個體,是以@Conditional标注在方法上隻能控制一個bean執行個體是否注入。

标注在類上:

一個類中可以注入很多執行個體,@Conditional标注在類上就決定了一批bean是否注入。

我們試一下,将BeanConfig改寫,這時,如果WindowsCondition傳回true,則兩個Person執行個體将被注入(注意:上一個測試将os.name改為linux,這是我将把這個參數去掉):

@Conditional({WindowsCondition.class})
@Configuration
public class BeanConfig {
 
    @Bean(name = "bill")
    public Person person1(){
        return new Person("Bill Gates",62);
    }
 
    @Bean("linus")
    public Person person2(){
        return new Person("Linus",48);
    }
}
           

結果兩個執行個體都被注入: 

如果将類上的WindowsCondition.class改為LinuxCondition.class,結果應該可以猜到:

結果就是空的,類中所有bean都沒有注入。

多個條件類:

前言中說,@Conditional注解傳入的是一個Class數組,存在多種條件類的情況。

這種情況貌似判斷難度加深了,測試一波,新增新的條件類,實作的matches傳回false(這種寫死傳回false的方法純屬測試用,沒有實際意義O(∩_∩)O)

public class ObstinateCondition implements Condition {
 
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
         return false;
    }
}
 BeanConfig修改一下:

@Conditional({WindowsCondition.class,ObstinateCondition.class})
@Configuration
public class BeanConfig {
 
    @Bean(name = "bill")
    public Person person1(){
        return new Person("Bill Gates",62);
    }
 
    @Bean("linus")
    public Person person2(){
        return new Person("Linus",48);
    }
}
           

結果:

現在如果将ObstinateCondition的matches方法傳回值改成true,兩個bean就被注入進容器:

結論得:

第一個條件類實作的方法傳回true,第二個傳回false,則結果false,不注入進容器。

第一個條件類實作的方法傳回true,第二個傳回true,則結果true,注入進容器中。

繼@Conditional注解後,又基于此注解推出了很多派生注解,比如@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnExpression、@ConditionalOnClass......動态注入bean變得更友善了。本篇将講解@ConditionalOnBean注解。

配置類中有兩個Computer類的bean,一個是筆記本電腦,一個是備用電腦。如果目前容器中已經有電腦bean了,就不注入備用電腦,如果沒有,則注入備用電腦,這裡需要使用到@ConditionalOnMissingBean。

@Configuration
public class BeanConfig {
 
    @Bean(name = "notebookPC")
    public Computer computer1(){
        return new Computer("筆記本電腦");
    }
 
    @ConditionalOnMissingBean(Computer.class)
    @Bean("reservePC")
    public Computer computer2(){
        return new Computer("備用電腦");
    }
}
           

這個注解就實作了功能,這個@ConditionalOnMissingBean為我們做了什麼呢?我們來一探究竟.。

一探究竟:

首先,來看@ConditionalOnMissingBean的聲明:

//可以标注在類和方法上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//使用了@Conditional注解,條件類是OnBeanCondition
@Conditional({OnBeanCondition.class})
public @interface ConditionalOnMissingBean {
    Class<?>[] value() default {};
 
    String[] type() default {};
 
    Class<?>[] ignored() default {};
 
    String[] ignoredType() default {};
 
    Class<? extends Annotation>[] annotation() default {};
 
    String[] name() default {};
 
    SearchStrategy search() default SearchStrategy.ALL;
}
           

這時候,我們就看到了我們熟悉的@Conditional注解,OnBeanCondition作為條件類。

OnBeanCondition類的聲明:

//定義帶注釋的元件的排序順序,2147483647即為預設值
@Order(2147483647)
class OnBeanCondition extends SpringBootCondition implements ConfigurationCondition {
           

它繼承了SpringBootCondition類,OnBeanCondition類中沒有matches方法,而SpringBootCondition類中有實作matches方法。OnBeanCondition還實作了ConfigurationCondition,ConfigurationCondition接口不熟悉的讀者可以到Spring ConfigurationCondition接口詳解 了解接口。OnBeanCondition類重寫了getConfigurationPhase()方法,表示在注冊bean的時候注解生效:

    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }
           

就從matches方法開始:

//SpringBootCondition類中的matches方法
    public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //擷取目前的類名或者方法名(由标注的位置決定)
        String classOrMethodName = getClassOrMethodName(metadata);
 
        try {
            //關鍵代碼:這裡就會判斷出結果
            ConditionOutcome outcome = this.getMatchOutcome(context, metadata);
            //存入日志
            this.logOutcome(classOrMethodName, outcome);
            //存入記錄
            this.recordEvaluation(context, classOrMethodName, outcome);
            //最後傳回ConditionOutcome的isMatch就是傳回boolean類型結果
            return outcome.isMatch();
        } catch (NoClassDefFoundError var5) {
            throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to " + var5.getMessage() + " not found. Make sure your own configuration does not rely on that class. This can also happen if you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)", var5);
        } catch (RuntimeException var6) {
            throw new IllegalStateException("Error processing condition on " + this.getName(metadata), var6);
        }
    }
           

關鍵代碼在OnBeanCondition的getMatchOutcome方法上:

/**
     * 獲得判斷結果的方法,ConditionOutcome類中存着boolean類型的結果
     */
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    //傳回一個新的ConditionMessage
        ConditionMessage matchMessage = ConditionMessage.empty();
        OnBeanCondition.BeanSearchSpec spec;
        List matching;
        //這是metadata會調用isAnnotated方法判斷目前标注的注解是不是ConditionalOnMissingBean
    //其實@ConditionalOnBean、@ConditionalOnMissingBean和@ConditionalOnSingleCandidate都是使用這個條件類,是以這裡做判斷
        if (metadata.isAnnotated(ConditionalOnBean.class.getName())) {
            spec = new OnBeanCondition.BeanSearchSpec(context, metadata, ConditionalOnBean.class);
            matching = this.getMatchingBeans(context, spec);
            if (matching.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnBean.class, new Object[]{spec}).didNotFind("any beans").atAll());
            }
 
            matchMessage = matchMessage.andCondition(ConditionalOnBean.class, new Object[]{spec}).found("bean", "beans").items(Style.QUOTE, matching);
        }
 
        if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
            OnBeanCondition.BeanSearchSpec spec = new OnBeanCondition.SingleCandidateBeanSearchSpec(context, metadata, ConditionalOnSingleCandidate.class);
            matching = this.getMatchingBeans(context, spec);
            if (matching.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, new Object[]{spec}).didNotFind("any beans").atAll());
            }
 
            if (!this.hasSingleAutowireCandidate(context.getBeanFactory(), matching, spec.getStrategy() == SearchStrategy.ALL)) {
                return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnSingleCandidate.class, new Object[]{spec}).didNotFind("a primary bean from beans").items(Style.QUOTE, matching));
            }
 
            matchMessage = matchMessage.andCondition(ConditionalOnSingleCandidate.class, new Object[]{spec}).found("a primary bean from beans").items(Style.QUOTE, matching);
        }
 
        //如果目前注入的bean是@ConditionalOnMissingBean
        if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
            //傳回一個spec(說明),這裡的spec規定了搜尋的内容,比如搜尋政策、需要搜尋的類名......
            spec = new OnBeanCondition.BeanSearchSpec(context, metadata, ConditionalOnMissingBean.class);
            //主要的搜尋實作在這個方法裡,最後傳回一個list
            matching = this.getMatchingBeans(context, spec);
            //判斷搜尋出來的結果
            if (!matching.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingBean.class, new Object[]{spec}).found("bean", "beans").items(Style.QUOTE, matching));
            }
 
            matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, new Object[]{spec}).didNotFind("any beans").atAll();
        }
 
        return ConditionOutcome.match(matchMessage);
    }
spec = new OnBeanCondition.BeanSearchSpec(context, metadata, ConditionalOnBean.class);
           

這句中,相當于從内部類中将标注@ConditionalOnMissingBean注解時的屬性都取出來:

BeanSearchSpec(ConditionContext context, AnnotatedTypeMetadata metadata, Class<?> annotationType) {
            this.annotationType = annotationType;
            MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(annotationType.getName(), true);
            //将attributes這個map中的資料放到對應的list成員變量中
            this.collect(attributes, "name", this.names);
            this.collect(attributes, "value", this.types);
            this.collect(attributes, "type", this.types);
            this.collect(attributes, "annotation", this.annotations);
            this.collect(attributes, "ignored", this.ignoredTypes);
            this.collect(attributes, "ignoredType", this.ignoredTypes);
            this.strategy = (SearchStrategy)metadata.getAnnotationAttributes(annotationType.getName()).get("search");
            OnBeanCondition.BeanTypeDeductionException deductionException = null;
 
            try {
                if (this.types.isEmpty() && this.names.isEmpty()) {
                    this.addDeducedBeanType(context, metadata, this.types);
                }
            } catch (OnBeanCondition.BeanTypeDeductionException var7) {
                deductionException = var7;
            }
 
            this.validate(deductionException);
        }
 
        //驗證的方法
        protected void validate(OnBeanCondition.BeanTypeDeductionException ex) {
            if (!this.hasAtLeastOne(this.types, this.names, this.annotations)) {
                String message = this.annotationName() + " did not specify a bean using type, name or annotation";
                if (ex == null) {
                    throw new IllegalStateException(message);
                } else {
                    throw new IllegalStateException(message + " and the attempt to deduce the bean's type failed", ex);
                }
            }
        }
           

看一下OnBeanCondition類中的getMatchingBeans方法,裡面有用到搜尋政策,詳見搜尋政策介紹

private List<String> getMatchingBeans(ConditionContext context, OnBeanCondition.BeanSearchSpec beans) {
        //獲得目前bean工廠
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //判斷目前的搜尋政策是否是PARENTS或者ANCESTORS,預設是ALL
        if (beans.getStrategy() == SearchStrategy.PARENTS || beans.getStrategy() == SearchStrategy.ANCESTORS) {
            BeanFactory parent = beanFactory.getParentBeanFactory();
            Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent, "Unable to use SearchStrategy.PARENTS");
            //如果是PARENTS或者ANCESTORS,目前bean工廠就用父工廠
            beanFactory = (ConfigurableListableBeanFactory)parent;
        }
 
        if (beanFactory == null) {
            return Collections.emptyList();
        } else {
            List<String> beanNames = new ArrayList();
            //如果目前搜尋政策等于CURRENT,為true
            boolean considerHierarchy = beans.getStrategy() != SearchStrategy.CURRENT;
            //這裡的type就是需要查找的bean的類型
            //下面,會從屬性中找bean
            Iterator var6 = beans.getTypes().iterator();
 
            String beanName;
            while(var6.hasNext()) {
                beanName = (String)var6.next();
                //如果找到了類型,接下來就是根據類型找bean的執行個體名,找示例名的方法在下方,實際上就是一個getNamesForType
                beanNames.addAll(this.getBeanNamesForType(beanFactory, beanName, context.getClassLoader(), considerHierarchy));
            }
 
            var6 = beans.getIgnoredTypes().iterator();
 
            while(var6.hasNext()) {
                beanName = (String)var6.next();
                beanNames.removeAll(this.getBeanNamesForType(beanFactory, beanName, context.getClassLoader(), considerHierarchy));
            }
 
            var6 = beans.getAnnotations().iterator();
 
            while(var6.hasNext()) {
                beanName = (String)var6.next();
                beanNames.addAll(Arrays.asList(this.getBeanNamesForAnnotation(beanFactory, beanName, context.getClassLoader(), considerHierarchy)));
            }
 
            var6 = beans.getNames().iterator();
 
            while(var6.hasNext()) {
                beanName = (String)var6.next();
                if (this.containsBean(beanFactory, beanName, considerHierarchy)) {
                    beanNames.add(beanName);
                }
            }
            //将存放bean執行個體名的list傳回
            return beanNames;
        }
    }
 
 
 
    //根據類型擷取bean的name
    private Collection<String> getBeanNamesForType(ListableBeanFactory beanFactory, String type, ClassLoader classLoader, boolean considerHierarchy) throws LinkageError {
        try {
            Set<String> result = new LinkedHashSet();
            this.collectBeanNamesForType(result, beanFactory, ClassUtils.forName(type, classLoader), considerHierarchy);
            return result;
        } catch (ClassNotFoundException var6) {
            return Collections.emptySet();
        } catch (NoClassDefFoundError var7) {
            return Collections.emptySet();
        }
    }
 
    private void collectBeanNamesForType(Set<String> result, ListableBeanFactory beanFactory, Class<?> type, boolean considerHierarchy) {
        result.addAll(BeanTypeRegistry.get(beanFactory).getNamesForType(type));
        if (considerHierarchy && beanFactory instanceof HierarchicalBeanFactory) {
            BeanFactory parent = ((HierarchicalBeanFactory)beanFactory).getParentBeanFactory();
            if (parent instanceof ListableBeanFactory) {
                this.collectBeanNamesForType(result, (ListableBeanFactory)parent, type, considerHierarchy);
            }
        }
 
    }
           

找完bean了之後,回到剛才的代碼裡:

//如果目前注入的bean是@ConditionalOnMissingBean
        if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
            //傳回一個spec(說明),這裡的spec規定了搜尋的内容,比如搜尋政策、需要搜尋的類名......
            spec = new OnBeanCondition.BeanSearchSpec(context, metadata, ConditionalOnMissingBean.class);
            matching = this.getMatchingBeans(context, spec);
            if (!matching.isEmpty()) {
                return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingBean.class, new Object[]{spec}).found("bean", "beans").items(Style.QUOTE, matching));
            }
 
            matchMessage = matchMessage.andCondition(ConditionalOnMissingBean.class, new Object[]{spec}).didNotFind("any beans").atAll();
        }
           

如果第5行傳回的list不是空的,就會傳回ConditionOutcome對象noMatch方法,表示不比對。ConditionOutcome類用于存放過濾結果,隻有兩個變量:

/**
 * 過濾結果類
 */
public class ConditionOutcome {
    /**
     * 比對結果 true or false
     */
    private final boolean match;
    /**
     * 比對結果資訊
     */
    private final ConditionMessage message;
           

兩者差別:

@ConditionOnBean在判斷list的時候,如果list沒有值,傳回false,否則傳回true

@ConditionOnMissingBean在判斷list的時候,如果list沒有值,傳回true,否則傳回false,其他邏輯都一樣

例子:

@ConditionalOnBean(javax.sql.DataSource.class)    

Spring容器或者所有父容器中需要存在至少一個javax.sql.DataSource類的執行個體

另外其他幾個擴充:

@ConditionalOnBean(僅僅在目前上下文中存在某個對象時,才會執行個體化一個Bean)
@ConditionalOnClass(某個class位于類路徑上,才會執行個體化一個Bean)
@ConditionalOnExpression(當表達式為true的時候,才會執行個體化一個Bean)
@ConditionalOnMissingBean(僅僅在目前上下文中不存在某個對象時,才會執行個體化一個Bean)
@ConditionalOnMissingClass(某個class類路徑上不存在的時候,才會執行個體化一個Bean)
@ConditionalOnNotWebApplication(不是web應用)
@ConditionalOnProperty 指在application.yml裡配置的屬性是否為true

           
@Configuration
@ConditionalOnProperty(
        value = {"abc.property"},
        matchIfMissing = false
)
@ConditionalOnClass(Abc.class)
public class Multi {
    @Bean
    @ConditionalOnMissingBean({Abc.class})
    public String check() {
        System.err.println("multi check");
        return "check";
    }
           

 在配置裡加上abc.property = true這個配置就可以測試上面的代碼了。

參考位址:

https://blog.csdn.net/xcy1193068639/article/details/81517456

https://blog.csdn.net/tianyaleixiaowu/article/details/78201587