? ?? Spring Boot可以使用條件裝配來靈活地指定什么時候?qū)⒛男゜ean實例化并納入容器纹坐,條件裝配是spring boot自動配置機制(auto configure)的重要一環(huán),也是理解spring boot原理的重要基礎(chǔ)收津。本文以實例為引導增淹,展示spring條件裝配的常用使用場景,其間也會涉及一些spring的原理。閱讀本文原杂,要求有一些spring和spring boot的基本使用經(jīng)驗挨决,最好對java config配置有一定了解请祖。
? ? 條件裝配主要以@ConditionalOnXXX系列注解和@Conditional注解兩大類注解結(jié)合java config配置來實現(xiàn)。其中@ConditionalOnXXX相當于@Conditional的多種預置特殊場景凰棉,提供裝配bean的多種特殊條件损拢。
? ? (一)實驗環(huán)境介紹
1. 搭建一個基本maven工程陌粹,包含spring容器的基本配置即可撒犀,無需web和jdbc相關(guān)依賴。為方便起見掏秩,你也可以使用spring boot向?qū)Э焖俅罱ㄒ粋€spring boot工程并導入依賴或舞,但是不要使用spring boot自帶的啟動類運行。我們會自己創(chuàng)建容器啟動類(也是我們的測試類)蒙幻,用比較原始的方式創(chuàng)建一個spring容器映凳,便于我們觀察bean是否被spring創(chuàng)建。
2. 實驗組件邮破。
? ? ? ?? 我們的實驗用組件就三大類诈豌,包含main方法的測試類,幾個java bean以及一個spring的配置類抒和。
? ?? 1)測試類矫渔。創(chuàng)建幾個包含主函數(shù)的普通java類即可,我們在main函數(shù)中手工創(chuàng)建spring容器摧莽,并用getBean方法獲取相應的bean并打印地址庙洼,觀察該bean是否被創(chuàng)建。當然镊辕,如果你想使用junit單元測試類也是可以的油够。我們這里設(shè)置了三個測試類,結(jié)構(gòu)和內(nèi)容都相似征懈,你也可以只創(chuàng)建一個石咬,我們只是為了測試代碼更清晰一些,因為要測試不同的條件裝配卖哎。具體代碼見下文詳細說明碌补。
實驗組件說明
? 2)幾個普通的java類,用于充當要納入spring容器管理的bean棉饶,無需繼承任何類和實現(xiàn)任何接口(pojo)
這里取名為C1厦章,C2。照藻。袜啃。,你也可以根據(jù)自己習慣自行取名幸缕。出于良好的編程習慣群发,可以顯式地增加一個無參構(gòu)造器晰韵,可以什么內(nèi)容都不寫,下面不再贅述熟妓。
public class C1 {
? ? public C1(){
? ? }
}
3) 一個spring 配置類?
這是我們實驗的重點雪猪,具體內(nèi)容在下節(jié)說明。
(二)編寫配置類ConditionalConfig
/**
* 條件裝配測試
* 使用conditional相關(guān)注解配置的一系列bean
*/
@Configuration
public class ConditionalConfig {
? ? /**
? ? * 無條件聲明bean
? ? * @return
*/
@Bean
public C1 getC1(){
? ? ? ? C1 c1 = new C1();
? ? ? ? return c1;
}
? ? ...
?? 該類用于完成所有實驗所用bean的配置起愈,也是個pojo只恨,不需要繼承任何類或?qū)崿F(xiàn)任何接口,但是需要以一個叫@Configuration的注解標注抬虽。這里使用了java config的配置方式官觅,是spring boot用注解代替XML,實現(xiàn)幾乎零配置的一個重要機制阐污。如果沒有接觸過java config的配置休涤,可以把該配置類想象成一個spring的xml配置文件。那么在這個配置文件里笛辟,最核心的標簽是什么呢功氨,當然就是<beans>,表示spring bean的容器手幢,里面會放置一個個的<bean>標簽捷凄,用于聲明要實例化和交由spring 容器管理的bean。同樣弯菊,我們這里的@Configuration注解就相當于<beans>標簽纵势,這個類里會有多個方法,以@Bean的注解標注管钳,看成一個個的
<bean>標簽钦铁,同樣用于聲明bean。也就是說兩種方式含義相同形式不同 ? ?
? ? ? ? 當然這里有個問題才漆,無論我們從容器獲取bean牛曹,還是進行依賴注入,通常都是使用bean的id醇滥。用XML配置文件的方式黎比,可以通過bean標簽的id屬性設(shè)置bean的id,那么使用java config的方式如果設(shè)置鸳玩?其實默認就是以每一個用于創(chuàng)建bean的方法名作為bean的id比如這里的getC1,getC2等等阅虫,隨后我們會在測試中驗證。如果你確實覺得這樣的名字比較奇怪不跟,也可以在@Bean注解中增加一個自定義bean id颓帝,比如myC1(實際上是為注解的value屬性賦值? 類似于spring mvc的requestmapping注解),就像這樣
@Bean("myC1")
public C1 getC1(){
? ? C1 c1 = new C1();
? ? return c1;
}
另外我們使用java config的方式在方法中實例化bean的方式通常就是直接new出來,要求有相應的構(gòu)造器
也可以通過bean所在類自己提供的工廠方法或者反射等其他方式來實例化bean购城,有興趣可以參閱其他資料吕座。實例化以后,該對象就會存在于當前spring容器中瘪板。對于c1 bean,我們讓它無條件創(chuàng)建
作為其他條件裝配bean的一個輔助條件? 下面介紹ConditionalOnXX系列注解
?? 1.? @ConditionalOnClass
該注解表示根據(jù)某個指定的類是否存在于classpath中來決定是否實例化一個bean吴趴。
Conditional} that only matches when the specified classes are on the classpath
注意該注解的target是type和method,也就是只能用于類型和方法上侮攀。這里我們注解在創(chuàng)建C2的java config方法上
@ConditionalOnClass(value = C1.class)// C1存在于classpath中才會加載C2
@Bean
public C2 getC2(){
? ? C2 c2 = new C2();
? ? return c2;
}
意思是要(調(diào)用該方法)創(chuàng)建C2锣枝,前提是C1的實例要存在于classpath中。存在則創(chuàng)建C2魏身,不存在則不創(chuàng)建惊橱,這就以另一個bean構(gòu)建了創(chuàng)建當前bean的條件蚪腐。運行程序觀察結(jié)果(注意測試代碼和運行方法會在下文介紹 這里只是通過控制臺輸出先給大家展示每種條件的含義 現(xiàn)在只需要知道我們是通過spring容器的getBean(bean id)方法獲取相應的bean的)
com.example.springbootconditiondemo.C1@5db250b4
com.example.springbootconditiondemo.C2@223f3642
Process finished with exit code 0
這里可以看到由于C1是無條件創(chuàng)建并納入spring容器的,所以肯定是存在于classpath中的箭昵,也就是說創(chuàng)建C2的前提條件是滿足的,所以C1,C2兩個實例均創(chuàng)建并納入spring容器回季。這也是比較簡單的一種條件裝配注解
?? 這是正向的例子 我們再來看下反向的例子? 也就是以一個不存在于當前classpath的類為條件
這里我們要修改@ConditionalOnClass注解的屬性
不再使用value, 而是使用name屬性指定一個字符串類型的全路徑類名 比如
/**
* 通過@ConditionalOnXX注解家制,指定是否加載該bean
* @return
*/
@ConditionalOnClass(name = "C8")// C8存在于classpath中才會加載C2
@Bean
public C2 getC2(){
? ? C2 c2 = new C2();
? ? return c2;
}
C8是不存在的一個類? 所以創(chuàng)建C2的條件不滿足? 我們再次進行測試
com.example.springbootconditiondemo.C1@184cf7cf
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.springbootconditiondemo.C2' available
?at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:346)
?at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:337)
?at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1123)
?at com.example.springbootconditiondemo.App.main(App.java:18)
可以看到c1依然可以正常創(chuàng)建 但是C2確無法創(chuàng)建
這就進一步體現(xiàn)了@ConditionalOnclass注解對創(chuàng)建某個bean的控制作用
2. @ConditionalOnBean
當指定的(另一個)bean存在于spring容器(上下文)中才執(zhí)行該方法加載bean,比如我們把剛才加載C2的方法修改一下,將@ConditionalOnClass注解替換成@ConditionalOnBean注解泡一。表示必須要C1 bean存在于當前spring 上下文中才會執(zhí)行該方法實例化C2
@ConditionalOnBean(value = C1.class) //C1存在于spring容器中才會加載C2
@Bean
public C2 getC2(){
? ? C2 c2 = new C2();
? ? return c2;
}
運行測試觀察結(jié)果:
17:26:26.230 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'getC4'
com.example.springbootconditiondemo.C1@45b9a632
com.example.springbootconditiondemo.C2@25d250c6
跟之前類似颤殴,C1,C2兩個bean均實例化并加載鼻忠。大家可能會有疑問涵但,這個注解同@ConditionalOnClass有什么區(qū)別呢。從目前來看帖蔓,只要C1類存在于classpath中矮瘟,無論是用@ConditionalOnClass注解還是@ConditionalOnBean注解,getC2()方法總會執(zhí)行塑娇,似乎沒什么區(qū)別澈侠。好,現(xiàn)在讓我們來做一個實驗埋酬。將getC1()方法上的@Bean注解注釋掉哨啃,同時注釋掉測試程序中獲取C1的getBean方法,其他地方不改写妥,運行測試程序
//@Bean("myC1")
public C1 getC1(){
? ? C1 c1 = new C1();
? ? return c1;
}
會發(fā)現(xiàn)出現(xiàn)異常拳球,無法獲取C2 bean。
No qualifying bean of type 'com.example.springbootconditiondemo.C2' available
原因是什么珍特,是因為C1 bean不存在于spring 容器中祝峻,基于@ConditionalOnBean指定的條件,必須要C1 bean存在于spring 容器中。我們前面說過呼猪,@Bean注解的含義同xml配置文件中<bean id=""/>一樣画畅,用于聲明一個bean,只有這樣聲明了 ,spring才會將其納入容器中宋距。那么此時C1這個類是否存在于classpath中呢轴踱,答案當然是肯定的,沒有實例在spring 容器中并不代表該類沒有被JVM加載谚赎。我們將getC2()方法上的條件注解再換回@ConditionalOnClass進行測試
@ConditionalOnClass(C1.class)
//@ConditionalOnClass(name = "C8")// C1存在于classpath中才會加載C2
// @ConditionalOnBean(value = C1.class) //C1存在于spring容器中才會加載C2
//@ConditionalOnMissingBean(value = C1.class)//C1不存在于spring容器中才會加載C2
@Bean
public C2 getC2(){
? ? C2 c2 = new C2();
? ? return c2;
}
會發(fā)現(xiàn)C2能夠正常從spring容器獲取
com.example.springbootconditiondemo.C2@4fe767f3
也就是getC2方法執(zhí)行的條件是滿足的淫僻,這就是@ConditionalOnClass和@ConditionalOnBean這兩個注解的區(qū)別
3. @ConditionalOnJava
這個條件注解比較簡單,顧名思義壶唤,判斷依據(jù)是當前jdk版本是否與注解中指定版本一致雳灵,一致則執(zhí)行,否則不執(zhí)行闸盔。來看例子:
/**
* 滿足指定java版本時加載
* @return
*
* value屬性指定jdk版本悯辙,range屬性指定是高于等于該版本還是小于該版本
*/
@Bean
@ConditionalOnJava(value = JavaVersion.EIGHT,range = ConditionalOnJava.Range.EQUAL_OR_NEWER)
public C4 getC4(){
? ? C4 c4 = new C4();
? ? return c4;
}
@ConditionalOnJava注解主要有兩個屬性,一個value指定jdk版本號迎吵,另一個是range,指示需要高于等于等于指定版本還是低于指定版本躲撰,用一個叫Range的枚舉表示,默認是前者击费。所以拢蛋,如果是這種情況,可以不配置range蔫巩,只配置value即可谆棱。這個注解比較好理解,就不演示測試結(jié)果了圆仔。
4. @Conditional
如果出現(xiàn)@ConditionalOnXX不能滿足要求垃瞧,通常是需要根據(jù)一些更復雜的業(yè)務邏輯判斷,那么可以使用@Conditional注解自定義判斷規(guī)則荧缘,來看配置的例子
/**
* 通過@Conditional注解皆警,自己指定是否加載該bean的邏輯
* @return
*/
@Bean
@Conditional(value = MyConditional1.class)
@Deprecated
public C3 getC3(){
? ? C3 c3 = new C3();
? ? return c3;
}
同樣注解到標注@Bean的方法上,具體的判斷邏輯由自定義的MyConditional1類來實現(xiàn)截粗。該類必須要實現(xiàn)org.springframework.context.annotation.Condition接口信姓,并且重寫其matches()方法,該方法返回boolean绸罗,用于只是條件是否滿足意推。
public class MyConditional1 implements Condition {
? ? /**
? ? * @param context? 應用上下文環(huán)境,可以通過該參數(shù)獲取用于幫助條件判斷的輔助類珊蟀,比如Environment,BeanFactory,ResouceLoader等
? ? * @param metadata 注解元數(shù)據(jù)菊值,用于獲取注解相關(guān)的信息
? ? * @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
?注意該方法的兩個參數(shù)外驱,這是我們獲取被標注對象的上下文和相關(guān)注解元信息的重要組件,我們的判斷邏輯通常圍繞著這兩個參數(shù)編寫腻窒。
1)ConditionContext 是應用上下文環(huán)境昵宇,我們可以通過該參數(shù)獲取用于幫助條件判斷的輔助類,比如Environment,BeanFactory,ResouceLoader等
2)注解元數(shù)據(jù)儿子,用于獲取被注解類或方法的其他注解相關(guān)的信息
? ? ? ? 來看例子瓦哎,首先來看ConditionContext的使用,這里我們通過該參數(shù)獲取spring的Environment接口柔逼,并通過該接口再獲取環(huán)境相關(guān)信息蒋譬,比如應用端口號,JAVA_HOME,MAVEN_HOME等信息都可以獲取
//1.通過context參數(shù)獲取Environment
Integer serverPort = context.getEnvironment().getProperty("server.port", Integer.TYPE);
System.out.println("當前應用端口號為:" + serverPort);
String mavenHome = context.getEnvironment().getProperty("MAVEN_HOME");
System.out.println("當前mavenhome:" + mavenHome);
測試結(jié)果:
當前應用端口號為:8888
19:08:35.027 [main] DEBUG org.springframework.core.env.PropertySourcesPropertyResolver - Found key 'MAVEN_HOME' in PropertySource 'systemEnvironment' with value of type String
當前mavenhome:G:\devSoftware\apache-maven-3.6.1
再來看另一個參數(shù)AnnotatedTypeMetadata愉适,它可以獲取被@Conditional標注的組件(一般是方法)上有哪些注解犯助,以及這些注解的元數(shù)據(jù)(屬性等)。比如我們這里測試該方法上是否有@Deprecated注解维咸,并作為Conditional的判斷條件剂买。如果有,則返回true,否則返回false腰湾。
metadata.isAnnotated(Deprecated.class.getName())
由于在前面的配置類中雷恃,對于C3實例化方法getC3()做了@Deprecated標注疆股,所以getBean時能獲取C3费坊,反之則不可以,請大家自行測試旬痹。
另外
ConditionContext 參數(shù)除了能獲取環(huán)境信息外附井,還可以獲取當前spring的BeanFactory(
context.getBeanFactory()
)以及classpath中的文件(
context.getResourceLoader();
)等,如果你需要使用這些信息進行Conditional判斷两残,請自行參考源碼永毅。
(三)編寫測試程序
最后,我們來展示一下如何編寫測試程序人弓。測試代碼很簡單沼死,就是一個普通類加一個main函數(shù),核心就是創(chuàng)建一個spring的應用上下文(也是BeanFactory)崔赌,Spring Boot是幫我們自動創(chuàng)建了意蛀。注意,這里的應用上下文(ApplicationContext)的實現(xiàn)類選用AnnotationConfigApplicationContext健芭,因為我們以注解的方式來配置县钥,而不是使用XML文檔形式。
public class App {
? ? public static void main(String[] args) {
? ? ? ? AnnotationConfigApplicationContext acx = new AnnotationConfigApplicationContext();
//注冊一個配置類慈迈,形成spring beans容器若贮,相當于加載xml配置文件
acx.register(ConditionalConfig.class);
//初始化一個spring 容器
acx.refresh();
//測試c1,c2兩個bean之間的依賴
? ? ? // System.out.println(acx.getBean(C1.class));
System.out.println(acx.getBean(C2.class));
}
}
上面的第二句就是指定我們的java config配置類,就是我們前面用的用@Configuration標注的那個ConditionalConfig谴麦,相當于加載一個XML格式的bean配置文件蠢沿。其他就是初始化容器,正常從容器中g(shù)etBean出你要找的實例(spring bean)匾效,跟XML配置的形式?jīng)]有太大區(qū)別搏予。我們這里使用了3個測試類只是為了更清晰,邏輯和功能基本一樣弧轧,完整源碼會放在附件中雪侥。
? ?? 以上就是我們簡單介紹的spring的Conditional條件裝配機制。限于篇幅精绎,只選擇了幾個常用的@ConditionalOnXX和@Conditional介紹速缨,大家在實際工作或閱讀spring boot源碼時可能還會遇到其他條件裝配標簽,可參考本文介紹思路和測試方法自行研究代乃。這些標簽使用本身不復雜旬牲,但是對具體含義和使用場景比較抽象,建議大家多動手搁吓,從正反兩方面測試原茅,便于理解和加深印象。