spring注解之@Conditional:通過條件來控制 bean 的注冊


參考:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648934205&idx=1&sn=5407aa7c49eb34f7fb661084b8873cfe&chksm=88621f03bf1596159eeb40d75620db03457f4aa831066052ebc6e1efc2d7b18802a49a7afe8a&token=332995799&lang=zh_CN&scene=21#wechat_redirect

一、面試阿里p7被問到的問題:

  1. @Conditional是做什么的?

  2. @Conditional多個條件是什么邏輯關系?

  3. 條件判斷在什么時候執行?

  4. ConfigurationCondition和Condition有什么區別?什么時候使用ConfigurationCondition?

  5. 多個Condition執行的順序是什么樣的?可以配置優先級么?

  6. 可以介紹一下@Conditional常見的一些用法么?

二、@Conditional注解

@Conditional注解是從spring4.0才有的,可以用在任何類型或者方法上面,通過@Conditional注解可以配置一些條件判斷,當所有條件都滿足的時候,被@Conditional標注的目標才會被spring容器處理。

比如可以通過@Conditional來控制bean是否需要注冊,控制被@Configuration標注的配置類是需要需要被解析等。

效果就像這段代碼,相當於在spring容器解析目標前面加了一個條件判斷:

if(@Conditional中配置的多個條件是否都匹配){
//spring繼續處理被@Conditional注解標注的對象
}

@Conditional源碼:

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

 

這個注解只有一個value參數,Condition類型的數組,Condition是一個接口,表示一個條件判斷,內部有個方法返回true或false,當所有Condition都成立的時候,@Conditional的結果才成立。

下面我們來看一下Condition接口。

三、Condition接口

用來表示條件判斷的接口,源碼如下:

@FunctionalInterface
public interface Condition {

    /**
     * 判斷條件是否匹配
     * context:條件判斷上下文
     */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

是一個函數式接口,內部只有一個matches方法,用來判斷條件是否成立的,2個參數:

  • context:條件上下文,ConditionContext接口類型的,可以用來獲取容器中的個人信息

  • metadata:用來獲取被@Conditional標注的對象上的所有注解信息

四、ConditionContext接口

這個接口中提供了一些常用的方法,可以用來獲取spring容器中的各種信息,看一下源碼:

public interface ConditionContext {

    /**
     * 返回bean定義注冊器,可以通過注冊器獲取bean定義的各種配置信息
     */
    BeanDefinitionRegistry getRegistry();

    /**
     * 返回ConfigurableListableBeanFactory類型的bean工廠,相當於一個ioc容器對象
     */
    @Nullable
    ConfigurableListableBeanFactory getBeanFactory();

    /**
     * 返回當前spring容器的環境配置信息對象
     */
    Environment getEnvironment();

    /**
     * 返回資源加載器
     */
    ResourceLoader getResourceLoader();

    /**
     * 返回類加載器
     */
    @Nullable
    ClassLoader getClassLoader();

}

 

五、比較關鍵性的問題:條件判斷在什么時候執行?

Spring對配置類的處理主要分為2個階段:

1、配置類解析階段

會得到一批配置類的信息,和一些需要注冊的bean

2、bean注冊階段

將配置類解析階段得到的配置類和需要注冊的bean注冊到spring容器中

3、看一下什么是配置類

類中有下面任意注解之一的就屬於配置類

  1. 類上有@Compontent注解

  2. 類上有@Configuration注解

  3. 類上有@CompontentScan注解

  4. 類上有@Import注解

  5. 類上有@ImportResource注解

  6. 類中有@Bean標注的方法

判斷一個類是不是一個配置類,是否的是下面這個方法,有興趣的可以看一下:

org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate

spring中處理這2個過程會循環進行,直到完成所有配置類的解析及所有bean的注冊。

4、Spring對配置類處理過程

源碼位置:

org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions

 

整個過程大致的過程如下:

  1. 通常我們會通過new AnnotationConfigApplicationContext()傳入多個配置類來啟動spring容器

  2. spring對傳入的多個配置類進行解析

  3. 配置類解析階段:這個過程就是處理配置類上面6中注解的過程,此過程中又會發現很多新的配置類,比如@Import導入的一批新的類剛好也符合配置類,而被@CompontentScan掃描到的一些類剛好也是配置類;此時會對這些新產生的配置類進行同樣的過程解析

  4. bean注冊階段:配置類解析后,會得到一批配置類和一批需要注冊的bean,此時spring容器會將這批配置類作為bean注冊到spring容器,同樣也會將這批需要注冊的bean注冊到spring容器

  5. 經過上面第3個階段之后,spring容器中會注冊很多新的bean,這些新的bean中可能又有很多新的配置類

  6. Spring從容器中將所有bean拿出來,遍歷一下,會過濾得到一批未處理的新的配置類,繼續交給第3步進行處理

  7. step3到step6,這個過程會經歷很多次,直到完成所有配置類的解析和bean的注冊

從上面過程中可以了解到:

  1. 可以在配置類上面加上@Conditional注解,來控制是否需要解析這個配置類,配置類如果不被解析,那么這個配置上面6種注解的解析都會被跳過

  2. 可以在被注冊的bean上面加上@Conditional注解,來控制這個bean是否需要注冊到spring容器中

  3. 如果配置類不會被注冊到容器,那么這個配置類解析所產生的所有新的配置類及所產生的所有新的bean都不會被注冊到容器

一個配置類被spring處理有2個階段:配置類解析階段、bean注冊階段(將配置類作為bean被注冊到spring容器)。

如果將Condition接口的實現類作為配置類上@Conditional中,那么這個條件會對兩個階段都有效,此時通過Condition是無法精細的控制某個階段的,如果想控制某個階段,比如可以讓他解析,但是不能讓他注冊,此時就就需要用到另外一個接口了:ConfigurationCondition

六、ConfigurationCondition接口

看一下這個接口的源碼:

public interface ConfigurationCondition extends Condition {

    /**
     * 條件判斷的階段,是在解析配置類的時候過濾還是在創建bean的時候過濾
     */
    ConfigurationPhase getConfigurationPhase();


    /**
     * 表示階段的枚舉:2個值
     */
    enum ConfigurationPhase {

        /**
         * 配置類解析階段,如果條件為false,配置類將不會被解析
         */
        PARSE_CONFIGURATION,

        /**
         * bean注冊階段,如果為false,bean將不會被注冊
         */
        REGISTER_BEAN
    }

}

ConfigurationCondition接口相對於Condition接口多了一個getConfigurationPhase方法,用來指定條件判斷的階段,是在解析配置類的時候過濾還是在創建bean的時候過濾。

七、@Conditional使用的3步驟

  1. 自定義一個類,實現Condition或ConfigurationCondition接口,實現matches方法

  2. 在目標對象上使用@Conditional注解,並指定value的指為自定義的Condition類型

  3. 啟動spring容器加載資源,此時@Conditional就會起作用了

八、案例1:阻止配置類的處理

在配置類上面使用@Conditional,這個注解的value指定的Condition當有一個為false的時候,spring就會跳過處理這個配置類。

自定義一個Condition類:

package com.javacode2018.lesson001.demo25.test3;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class MyCondition1 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return false;
    }
}

matches方法內部我們可以隨意發揮,此處為了演示效果就直接返回false。

來個配置類,在配置類上面使用上面這個條件,此時會讓配置類失效,如下:

package com.javacode2018.lesson001.demo25.test3;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Conditional(MyCondition1.class) //@1
@Configuration
public class MainConfig3 {
    @Bean
    public String name() { //@1
        return "路人甲Java";
    }
}

@1:使用了自定義的條件類

@2:通過@Bean標注這name這個方法,如果這個配置類成功解析,會將name方法的返回值作為bean注冊到spring容器

來個測試類,啟動spring容器加載MainConfig3配置類,如下:

package com.javacode2018.lesson001.demo25;

import com.javacode2018.lesson001.demo25.test3.MainConfig3;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.Map;

public class ConditionTest {

    @Test
    public void test3() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig3.class);
        Map<String, String> serviceMap = context.getBeansOfType(String.class);
        serviceMap.forEach((beanName, bean) -> {
            System.out.println(String.format("%s->%s", beanName, bean));
        });
    }
}

test3中,從容器中獲取String類型的bean,運行test3沒有任何輸出。

我們可以將MainConfig3上面的@Conditional去掉,再次運行輸出:

name->路人甲Java

九、案例2:阻止bean的注冊

來個配置類,如下:

package com.javacode2018.lesson001.demo25.test4;

import com.javacode2018.lesson001.demo25.test3.MyCondition1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MainConfig4 {
    @Conditional(MyCondition1.class) //@1
    @Bean
    public String name() {
        return "路人甲Java";
    }

    @Bean
    public String address() {
        return "上海市";
    }
}

上面2個方法上面使用了@Bean注解來定義了2個bean,name方法上面使用了@Conditional注解,這個條件會在name這個bean注冊到容器之前會進行判斷,當條件為true的時候,name這個bean才會被注冊到容器。

ConditionTest中新增個測試用例來加載上面這個配置類,從容器中獲取String類型所有bean輸出,代碼如下:

@Test
public void test4() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig4.class);
    Map<String, String> serviceMap = context.getBeansOfType(String.class);
    serviceMap.forEach((beanName, bean) -> {
        System.out.println(String.format("%s->%s", beanName, bean));
    });
}

運行輸出:

address->上海市

可以看到容器中只有一個address被注冊了,而name這個bean沒有被注冊。

十、案例3:bean不存在的時候才注冊

1、需求

IService接口有兩個實現類Service1和Service1,這兩個類會放在2個配置類中通過@Bean的方式來注冊到容器,此時我們想加個限制,只允許有一個IService類型的bean被注冊到容器。

可以在@Bean標注的2個方法上面加上條件限制,當容器中不存在IService類型的bean時,才將這個方法定義的bean注冊到容器,下面來看代碼實現。

2、代碼實現

條件判斷類:OnMissingBeanCondition

package com.javacode2018.lesson001.demo25.test1;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Map;

public class OnMissingBeanCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //獲取bean工廠
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //從容器中獲取IService類型bean
        Map<String, IService> serviceMap = beanFactory.getBeansOfType(IService.class);
        //判斷serviceMap是否為空
        return serviceMap.isEmpty();
    }

}

上面matches方法中會看容器中是否存在IService類型的bean,不存在的時候返回true

IService接口

package com.javacode2018.lesson001.demo25.test1;

public interface IService {
}

接口有2個實現類

Service1

package com.javacode2018.lesson001.demo25.test1;

public class Service1 implements IService {
}

Service2

package com.javacode2018.lesson001.demo25.test1;

public class Service2 implements IService {
}

來一個配置類負責注冊Service1到容器

package com.javacode2018.lesson001.demo25.test1;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig1 {
    @Conditional(OnMissingBeanCondition.class) //@1
    @Bean
    public IService service1() {
        return new Service1();
    }
}

@1:方法之前使用了條件判斷

再來一個配置類負責注冊Service2到容器

package com.javacode2018.lesson001.demo25.test1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig2 {
    @Conditional(OnMissingBeanCondition.class)//@1
    @Bean
    public IService service2() {
        return new Service2();
    }
}

@1:方法之前使用了條件判斷

來一個總的配置類,導入另外2個配置類

package com.javacode2018.lesson001.demo25.test1;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({BeanConfig1.class,BeanConfig2.class}) //@1
public class MainConfig1 {
}

@1:通過@Import將其他2個配置類導入

來個測試用例

ConditionTest新增一個方法,方法中從容器中獲取IService類型的bean,然后輸出:

@Test
public void test1() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig1.class);
    Map<String, IService> serviceMap = context.getBeansOfType(IService.class);
    serviceMap.forEach((beanName, bean) -> {
        System.out.println(String.format("%s->%s", beanName, bean));
    });
}

運行輸出:

service1->com.javacode2018.lesson001.demo25.test1.Service1@2cd76f31

可以看出容器中只有一個IService類型的bean。

可以將@Bean標注的2個方法上面的@Conditional去掉,再運行會輸出:

service1->com.javacode2018.lesson001.demo25.test1.Service1@49438269
service2->com.javacode2018.lesson001.demo25.test1.Service2@ba2f4ec

此時沒有條件限制,2個Service都會注冊到容器。

十一、案例4:根據環境選擇配置類

平常我們做項目的時候,有開發環境、測試環境、線上環境,每個環境中有些信息是不一樣的,比如數據庫的配置信息,下面我們來模擬不同環境中使用不同的配置類來注冊不同的bean。

1、自定義一個條件的注解

package com.javacode2018.lesson001.demo25.test2;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Conditional(EnvCondition.class) //@1
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnvConditional {
    //環境(測試環境、開發環境、生產環境)
    enum Env { //@2
        TEST, DEV, PROD
    }

    //環境
    Env value() default Env.DEV; //@3
}

@1:注意這個注解比較特別,這個注解上面使用到了@Conditional注解,這個地方使用到了一個自定義Conditione類:EnvCondition

@2:枚舉,表示環境,定義了3個環境

@3:這個參數用指定環境

上面這個注解一會我們會用在不同環境的配置類上面

2、下面來3個配置類

讓3個配置類分別在不同環境中生效,會在這些配置類上面使用上面自定義的@EnvConditional注解來做條件限定。

每個配置類中通過@Bean來定義一個名稱為name的bean,一會通過輸出這個bean來判斷哪個配置類生效了。

下面來看3個配置類的代碼

測試環境配置類

package com.javacode2018.lesson001.demo25.test2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnvConditional(EnvConditional.Env.TEST)//@1
public class TestBeanConfig {
    @Bean
    public String name() {
        return "我是測試環境!";
    }
}

@1指定的測試環境

開發環境配置類

package com.javacode2018.lesson001.demo25.test2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnvConditional(EnvConditional.Env.DEV) //@1
public class DevBeanConfig {
    @Bean
    public String name() {
        return "我是開發環境!";
    }
}

@1:指定的開發環境

生產環境配置類

package com.javacode2018.lesson001.demo25.test2;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnvConditional(EnvConditional.Env.PROD) //@1
public class ProdBeanConfig {
    @Bean
    public String name() {
        return "我是生產環境!";
    }
}

@1:指定的生產環境

下面來看一下條件類:EnvCondition

條件類會解析配置類上面@EnvConditional注解,得到環境信息。

然后和目前的環境對比,決定返回true還是false,如下:

package com.javacode2018.lesson001.demo25.test2;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class EnvCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //當前需要使用的環境
        EnvConditional.Env curEnv = EnvConditional.Env.DEV; //@1
        //獲取使用條件的類上的EnvCondition注解中對應的環境
        EnvConditional.Env env = (EnvConditional.Env) metadata.getAllAnnotationAttributes(EnvConditional.class.getName()).get("value").get(0);
        return env.equals(curEnv);
    }

}

@1:這個用來指定當前使用的環境,此處假定當前使用的是開發環境,這個我們以后可以任意發揮,比如將這些放到配置文件中,此處方便演示效果。

來個測試用例

@Test
public void test2() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig2.class);
    System.out.println(context.getBean("name"));
}

運行輸出

我是開發環境!

可以看到開發環境生效了。

修改一下EnvCondition的代碼,切換到生產環境:

EnvConditional.Env curEnv = EnvConditional.Env.PROD;

再次運行test2方法輸出:

我是生產環境!

生產環境配置類生效了。

十二、案例5:Condition指定優先級

1、多個Condition按順序執行

@Condtional中value指定多個Condtion的時候,默認情況下會按順序執行,還是通過代碼來看一下效果。

下面代碼中定義了3個Condition,每個Condition的matches方法中會輸出當前類名,然后在配置類上面同時使用這3個Condition:

package com.javacode2018.lesson001.demo25.test5;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;

class Condition1 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }
}

class Condition2 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }
}

class Condition3 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }
}

@Configuration
@Conditional({Condition1.class, Condition2.class, Condition3.class})
public class MainConfig5 {
}

來個測試用例

@Test
public void test5() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig5.class);
}

運行輸出:

com.javacode2018.lesson001.demo25.test5.Condition1
com.javacode2018.lesson001.demo25.test5.Condition2
com.javacode2018.lesson001.demo25.test5.Condition3
com.javacode2018.lesson001.demo25.test5.Condition1
com.javacode2018.lesson001.demo25.test5.Condition2
com.javacode2018.lesson001.demo25.test5.Condition3
com.javacode2018.lesson001.demo25.test5.Condition1
com.javacode2018.lesson001.demo25.test5.Condition2
com.javacode2018.lesson001.demo25.test5.Condition3

上面有多行輸出,是因為spring解析整個配置類的過程中,有好幾個地方都會執行條件判斷。

咱們只用關注前3行,可以看出輸出的屬性和@Conditional中value值的順序是一樣的。

2、指定Condition的順序

自定義的Condition可以實現PriorityOrdered接口或者繼承Ordered接口,或者使用@Order注解,通過這些來指定這些Condition的優先級。

排序規則:先按PriorityOrdered排序,然后按照order的值進行排序;也就是:PriorityOrdered asc,order值 asc

下面這幾個都可以指定order的值
接口:org.springframework.core.Ordered,有個getOrder方法用來返回int類型的值
接口:org.springframework.core.PriorityOrdered,繼承了Ordered接口,所以也有getOrder方法
注解:org.springframework.core.annotation.Order,有個int類型的value參數指定Order的大小

看案例代碼:

package com.javacode2018.lesson001.demo25.test6;


import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;

@Order(1) //@1
class Condition1 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }
}

class Condition2 implements Condition, Ordered { //@2
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

    @Override
    public int getOrder() { //@3
        return 0;
    }
}

class Condition3 implements Condition, PriorityOrdered { //@4
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        System.out.println(this.getClass().getName());
        return true;
    }

    @Override
    public int getOrder() {
        return 1000;
    }
}

@Configuration
@Conditional({Condition1.class, Condition2.class, Condition3.class})//@5
public class MainConfig6 {
}

@1:Condition1通過@Order指定順序,值為1

@2:Condition2通過實現了Ordered接口來指定順序,@3:getOrder方法返回1

@4:Condition3實現了PriorityOrdered接口,實現這個接口需要重寫getOrder方法,返回1000

@5:Condtion順序為1、2、3

根據排序的規則,PriorityOrdered的會排在前面,然后會再按照order升序,最后可以順序是:

Condtion3->Condtion2->Condtion1

來個測試用例看看效果是不是我們分析的這樣:

@Test
public void test6() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig6.class);
}

運行test6,部分輸出如下:

com.javacode2018.lesson001.demo25.test6.Condition3
com.javacode2018.lesson001.demo25.test6.Condition2
com.javacode2018.lesson001.demo25.test6.Condition1

結果和我們分析的一致。

十三、案例6:ConfigurationCondition使用

ConfigurationCondition使用的比較少,很多地方對這個基本上也不會去介紹,Condition接口基本上可以滿足99%的需求了,但是springboot中卻大量用到了ConfigurationCondition這個接口。

ConfigurationCondition通過解釋比較難理解,來個案例感受一下:

1、來一個普通的類:Service

package com.javacode2018.lesson001.demo25.test7;

public class Service {
}

2、來一個配置類,通過配置類注冊上面這個Service

package com.javacode2018.lesson001.demo25.test7;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig1 {
    @Bean
    public Service service() {
        return new Service();
    }
}

3、再來一個配置類:BeanConfig2

package com.javacode2018.lesson001.demo25.test7;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig2 {
    @Bean
    public String name() {
        return "路人甲Java";
    }
}

4、來一個總的配置類

package com.javacode2018.lesson001.demo25.test7;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({BeanConfig1.class, BeanConfig2.class})
public class MainConfig7 {
}

上面通過@Import引入了另外2個配置類

5、來個測試用例加載MainConfig7配置類

@Test
public void test7() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfig7.class);
    context.getBeansOfType(String.class).forEach((beanName, bean) -> {
        System.out.println(String.format("%s->%s", beanName, bean));
    });
}

上面從容器中獲取String類型的bean,然后輸出。

6、運行輸出

name->路人甲Java

7、現在我們有個需求

當容器中有Service這種類型的bean的時候,BeanConfig2才生效。

很簡單吧,加個Condition就行了,內部判斷容器中是否有Service類型的bean,繼續

8、來個自定義的Condition

package com.javacode2018.lesson001.demo25.test7;


import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class MyCondition1 implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //獲取spring容器
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //判斷容器中是否存在Service類型的bean
        boolean existsService = !beanFactory.getBeansOfType(Service.class).isEmpty();
        return existsService;
    }
}

上面代碼很簡單,判斷容器中是否有IService類型的bean。

9、BeanConfig2上使用Condition條件判斷

@Configuration
@Conditional(MyCondition1.class)
public class BeanConfig2 {
    @Bean
    public String name() {
        return "路人甲Java";
    }
}

10、再次運行test7輸出

無任何輸出

11、為什么?

在文章前面我們說過,配置類的處理會依次經過2個階段:配置類解析階段和bean注冊階段,Condition接口類型的條件會對這兩個階段都有效,解析階段的時候,容器中是還沒有Service這個bean的,配置類中通過@Bean注解定義的bean在bean注冊階段才會被注冊到spring容器,所以BeanConfig2在解析階段去容器中是看不到Service這個bean的,所以就被拒絕了。

此時我們需要用到ConfigurationCondition了,讓條件判斷在bean注冊階段才起效。

12、自定義一個ConfigurationCondition類

package com.javacode2018.lesson001.demo25.test7;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.ConfigurationCondition;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class MyConfigurationCondition1 implements ConfigurationCondition {
    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN; //@1
    }

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        //獲取spring容器
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        //判斷容器中是否存在Service類型的bean
        boolean existsService = !beanFactory.getBeansOfType(Service.class).isEmpty();
        return existsService;
    }
}

@1:指定條件在bean注冊階段,這個條件才有效

matches方法中的內容直接復制過來,判斷規則不變。

13、修改BeanConfig2的類容

將
@Conditional(MyCondition1.class)
替換為
@Conditional(MyConfigurationCondition1.class)

14、再次運行test7輸出

name->路人甲Java

此時name這個bean被輸出了。

可以再試試將BeanConfig1中service方法上面的@Bean去掉,此時Service就不會被注冊到容器,再運行一下test7,會發現沒有輸出了,此時BeanConfig2會失效。

判斷bean存不存在的問題,通常會使用ConfigurationCondition這個接口,階段為:REGISTER_BEAN,這樣可以確保條件判斷是在bean注冊階段執行的。

對springboot比較熟悉的,它里面有很多@Conditionxxx這樣的注解,可以去看一下這些注解,很多都實現了ConfigurationCondition接口。

十四、Spring中這塊的源碼

@Conditional注解是被下面這個類處理的

org.springframework.context.annotation.ConfigurationClassPostProcessor

又是這個類,說了很多次了,非常重要的一個類,大家下去了多擼一下這個類的源碼,這樣理解起來更順暢一些。

案例源碼

https://gitee.com/javacode2018/spring-series

總結

  1. @Conditional注解可以標注在spring需要處理的對象上(配置類、@Bean方法),相當於加了個條件判斷,通過判斷的結果,讓spring覺得是否要繼續處理被這個注解標注的對象

  2. spring處理配置類大致有2個過程:解析配置類、注冊bean,這兩個過程中都可以使用@Conditional來進行控制spring是否需要處理這個過程

  3. Condition默認會對2個過程都有效

  4. ConfigurationCondition控制得更細一些,可以控制到具體那個階段使用條件判斷

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM