@Conditional注解是從spring4.0才有的,可以用在任何類型或者方法上面,通過@Conditional注解可以配置一些條件判斷,當所有條件都滿足的時候,被@Conditional標注的目標才會被spring容器處理。
@Conditional源碼:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {Class<? extends Condition>[] value();
}
這個注解只有一個value參數,Condition類型的數組,Condition是一個接口,表示一個條件判斷,內部有個方法返回true或false,當所有Condition都成立的時候,@Conditional的結果才成立。
spring是如何處理注解的。用來表示條件判斷的接口,源碼如下:
@FunctionalInterface
public interface Condition {/*** 判斷條件是否匹配* context:條件判斷上下文*/boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
是一個函數式接口,內部只有一個matches方法,用來判斷條件是否成立的,2個參數:
這個接口中提供了一些常用的方法,可以用來獲取spring容器中的各種信息,看一下源碼:
public interface ConditionContext {/*** 返回bean定義注冊器,可以通過注冊器獲取bean定義的各種配置信息*/BeanDefinitionRegistry getRegistry();/*** 返回ConfigurableListableBeanFactory類型的bean工廠,相當于一個ioc容器對象*/@NullableConfigurableListableBeanFactory getBeanFactory();/*** 返回當前spring容器的環境配置信息對象*/Environment getEnvironment();/*** 返回資源加載器*/ResourceLoader getResourceLoader();/*** 返回類加載器*/@NullableClassLoader getClassLoader();
}
Spring常用注解,配置類解析階段
會得到一批配置類的信息,和一些需要注冊的bean
bean注冊階段
將配置類解析階段得到的配置類和需要注冊的bean注冊到spring容器中
類上有@Compontent注解
類上有@Configuration注解
類上有@CompontentScan注解
類上有@Import注解
類上有@ImportResource注解
類中有@Bean標注的方法
判斷一個類是不是一個配置類,是否的是下面這個方法,有興趣的可以看一下:
org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate
spring中處理這2個過程會循環進行,直到完成所有配置類的解析及所有bean的注冊。
Spring對配置類處理過程
源碼位置:
org.springframework.context.annotation.ConfigurationClassPostProcessor#processConfigBeanDefinitions
整個過程大致的過程如下:
通常我們會通過new AnnotationConfigApplicationContext()傳入多個配置類來啟動spring容器
spring對傳入的多個配置類進行解析
配置類解析階段:這個過程就是處理配置類上面6中注解的過程,此過程中又會發現很多新的配置類,比如@Import導入的一批新的類剛好也符合配置類,而被@CompontentScan掃描到的一些類剛好也是配置類;此時會對這些新產生的配置類進行同樣的過程解析
bean注冊階段:配置類解析后,會得到一批配置類和一批需要注冊的bean,此時spring容器會將這批配置類作為bean注冊到spring容器,同樣也會將這批需要注冊的bean注冊到spring容器
經過上面第3個階段之后,spring容器中會注冊很多新的bean,這些新的bean中可能又有很多新的配置類
Spring從容器中將所有bean拿出來,遍歷一下,會過濾得到一批未處理的新的配置類,繼續交給第3步進行處理
step3到step6,這個過程會經歷很多次,直到完成所有配置類的解析和bean的注冊
從上面過程中可以了解到:
可以在配置類上面加上@Conditional注解,來控制是否需要解析這個配置類,配置類如果不被解析,那么這個配置上面6種注解的解析都會被跳過
可以在被注冊的bean上面加上@Conditional注解,來控制這個bean是否需要注冊到spring容器中
如果配置類不會被注冊到容器,那么這個配置類解析所產生的所有新的配置類及所產生的所有新的bean都不會被注冊到容器
一個配置類被spring處理有2個階段:
如果將Condition接口的實現類作為配置類上@Conditional中,那么這個條件會對兩個階段都有效,此時通過Condition是無法精細的控制某個階段的,如果想控制某個階段,比如可以讓他解析,但是不能讓他注冊,此時就就需要用到另外一個接口了: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步驟
在配置類上面使用@Conditional,這個注解的value指定的Condition當有一個為false的時候,spring就會跳過處理這個配置類。
自定義一個Condition類:
package com.yuan11.annotation;import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;/*** @title: MyCondition1* @Author yuan11* @Date: 2022/6/27 23:57* @Version 1.0*/
public class MyCondition1 implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {return false;}
}
matches方法內部我們可以隨意發揮,此處為了演示效果就直接返回false。
來個配置類,在配置類上面使用上面這個條件,此時會讓配置類失效,如下:
package com.yuan11.annotation;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;/*** @title: Config3* @Author yuan11* @Date: 2022/6/27 23:59* @Version 1.0*/
@Conditional(MyCondition1.class) //@1
@Configuration
public class Config3 {@Beanpublic String name() { //@2return "yuan11";}
}
@1:使用了自定義的條件類
@2:通過@Bean標注這name這個方法,如果這個配置類成功解析,會將name方法的返回值作為bean注冊到spring容器
public class Test {public static void main(String[] args) {//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Conditional標注的類AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config3.class);Map<String, String> serviceMap = context.getBeansOfType(String.class);serviceMap.forEach((beanName, bean) -> {System.out.println(String.format("%s->%s", beanName, bean));});}
}
從容器中獲取String類型的bean,運行方法沒有任何輸出。
我們可以將Config3上面的@Conditional去掉,再次運行輸出:
name->yuan11
package com.yuan11.annotation;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;/*** @title: Config4* @Author yuan11* @Date: 2022/6/28 0:05* @Version 1.0*/
@Configuration
public class Config4 {@Conditional(MyCondition1.class) //@1@Beanpublic String name() {return "yuan11";}@Beanpublic String address() {return "西安市";}
}
上面2個方法上面使用了@Bean注解來定義了2個bean,name方法上面使用了@Conditional注解,這個條件會在name這個bean注冊到容器之前會進行判斷,當條件為true的時候,name這個bean才會被注冊到容器。
public class Test {public static void main(String[] args) {//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標注的類AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config4.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沒有被注冊。
可以在@Bean標注的2個方法上面加上條件限制,當容器中不存在IService類型的bean時,才將這個方法定義的bean注冊到容器,下面來看代碼實現。
package com.yuan11.annotation;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;import java.util.Map;/*** @title: OnMissingBeanCondition* @Author yuan11* @Date: 2022/6/28 0:17* @Version 1.0*/
public class OnMissingBeanCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {//獲取bean工廠ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();//從容器中獲取IService類型beanMap<String, IService> serviceMap = beanFactory.getBeansOfType(IService.class);//判斷serviceMap是否為空return serviceMap.isEmpty();}
}
上面matches方法中會看容器中是否存在IService類型的bean,不存在的時候返回true
IService接口
package com.yuan11.annotation;
public interface IService {
}
接口有2個實現類
Service1
package com.yuan11.annotation;
public class Service1 implements IService {
}
Service2
package com.yuan11.annotation;
public class Service2 implements IService {
}
來一個配置類負責注冊Service1、Service2到容器
@Configuration
public class BeanConfig1 {@Conditional(OnMissingBeanCondition.class) //@1@Beanpublic IService service1() {return new Service1();}
}
@Configuration
public class BeanConfig2 {@Conditional(OnMissingBeanCondition.class) //@1@Beanpublic IService service2() {return new Service2();}
}
@1:方法之前使用了條件判斷
來一個總的配置類,導入另外2個配置類
package com.yuan11.annotation;import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;/*** @title: Config5* @Author yuan11* @Date: 2022/6/28 0:25* @Version 1.0*/
@Configuration
@Import({BeanConfig1.class,BeanConfig2.class}) //@1
public class Config5 {
}
@1:通過@Import將其他2個配置類導入
-Test
public class Test {public static void main(String[] args) {//1.通過AnnotationConfigApplicationContext創建spring容器,參數為@Import標注的類AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config5.class);Map<String, IService> serviceMap = context.getBeansOfType(IService.class);serviceMap.forEach((beanName, bean) -> {System.out.println(String.format("%s->%s", beanName, bean));});}
}
運行輸出:
service1->com.yuan11.annotation.Service1@58c1c010
可以看出容器中只有一個IService類型的bean。
可以將@Bean標注的2個方法上面的@Conditional去掉,再運行會輸出:
service1->com.yuan11.annotation.Service1@3d36e4cd
service2->com.yuan11.annotation.Service2@6a472554
平常我們做項目的時候,有開發環境、測試環境、線上環境,每個環境中有些信息是不一樣的,比如數據庫的配置信息,下面我們來模擬不同環境中使用不同的配置類來注冊不同的bean。
自定義一個條件的注解
package com.yuan11.annotation;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;/*** @title: EnvConditional* @Author yuan11* @Date: 2022/6/28 0:44* @Version 1.0*/
@Conditional(EnvCondition.class) //@1
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnvConditional {//環境(測試環境、開發環境、生產環境)enum Env { //@2TEST, DEV, PROD}//環境Env value() default Env.DEV; //@3
}
@1:注意這個注解比較特別,這個注解上面使用到了@Conditional注解,這個地方使用到了一個自定義Conditione類:EnvCondition
@2:枚舉,表示環境,定義了3個環境
@3:這個參數用指定環境
上面這個注解一會我們會用在不同環境的配置類上面
下面來3個配置類
讓3個配置類分別在不同環境中生效,會在這些配置類上面使用上面自定義的@EnvConditional注解來做條件限定。
每個配置類中通過@Bean來定義一個名稱為name的bean,一會通過輸出這個bean來判斷哪個配置類生效了。
下面來看3個配置類的代碼
@Configuration
@EnvConditional(EnvConditional.Env.TEST)//@1
public class TestBeanConfig {@Beanpublic String name() {return "我是測試環境!";}
}
@1指定的測試環境
@Configuration
@EnvConditional(EnvConditional.Env.DEV)//@1
public class DevBeanConfig {@Beanpublic String name() {return "我是開發環境!";}
}
@1指定的開發環境
@Configuration
@EnvConditional(EnvConditional.Env.PROD)//@1
public class ProdBeanConfig {@Beanpublic String name() {return "我是生產環境!";}
}
@1指定的生產環境
條件類:EnvCondition
條件類會解析配置類上面@EnvConditional注解,得到環境信息。
然后和目前的環境對比,決定返回true還是false
public class EnvCondition implements Condition {@Overridepublic 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:這個用來指定當前使用的環境,此處假定當前使用的是開發環境,這個我們以后可以任意發揮,比如將這些放到配置文件中,此處方便演示效果。
@Configuration
@Import({TestBeanConfig.class, DevBeanConfig.class, ProdBeanConfig.class})
public class Config6 {
}
總配置類,導入環境配置
-Test
public class Test {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config6.class);System.out.println(context.getBean("name"));}
}
運行輸出
我是開發環境!
可以看到開發環境生效了。
修改一下EnvCondition的代碼,切換到生產環境:
EnvConditional.Env curEnv = EnvConditional.Env.PROD;
再次運行test方法輸出:
我是生產環境!
生產環境配置類生效了
@Condtional中value指定多個Condtion的時候,默認情況下會按順序執行
指定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的大小
@Order(1) //@1
class Condition1 implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {System.out.println(this.getClass().getName());return true;}
}
class Condition2 implements Condition, Ordered { //@2@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {System.out.println(this.getClass().getName());return true;}@Overridepublic int getOrder() { //@3return 0;}
}
class Condition3 implements Condition, PriorityOrdered { //@4@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {System.out.println(this.getClass().getName());return true;}@Overridepublic 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
ConfigurationCondition使用的比較少,很多地方對這個基本上也不會去介紹,Condition接口基本上可以滿足99%的需求了,但是springboot中卻大量用到了ConfigurationCondition這個接口。
判斷bean存不存在的問題,通常會使用ConfigurationCondition這個接口,階段為:REGISTER_BEAN,這樣可以確保條件判斷是在bean注冊階段執行的。
對springboot比較熟悉的,它里面有很多@Conditionxxx這樣的注解,可以去看一下這些注解,很多都實現了ConfigurationCondition接口。
Spring中這塊的源碼
@Conditional注解是被下面這個類處理的
org.springframework.context.annotation.ConfigurationClassPostProcessor
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态