Spring Boot原理解析之Conditional條件裝配

? ?? 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源碼時可能還會遇到其他條件裝配標簽,可參考本文介紹思路和測試方法自行研究代乃。這些標簽使用本身不復雜旬牲,但是對具體含義和使用場景比較抽象,建議大家多動手搁吓,從正反兩方面測試原茅,便于理解和加深印象。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末堕仔,一起剝皮案震驚了整個濱河市擂橘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌摩骨,老刑警劉巖通贞,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恼五,居然都是意外死亡昌罩,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進店門灾馒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茎用,“玉大人,你說我怎么就攤上這事睬罗」旃Γ” “怎么了?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵傅物,是天一觀的道長夯辖。 經(jīng)常有香客問我,道長董饰,這世上最難降的妖魔是什么蒿褂? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任圆米,我火速辦了婚禮,結(jié)果婚禮上啄栓,老公的妹妹穿的比我還像新娘娄帖。我一直安慰自己,他們只是感情好昙楚,可當我...
    茶點故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著堪旧,像睡著了一般削葱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淳梦,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天析砸,我揣著相機與錄音,去河邊找鬼爆袍。 笑死首繁,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的陨囊。 我是一名探鬼主播弦疮,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蜘醋!你這毒婦竟也來了胁塞?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤堂湖,失蹤者是張志新(化名)和其女友劉穎闲先,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體无蜂,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年蒙谓,在試婚紗的時候發(fā)現(xiàn)自己被綠了斥季。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡累驮,死狀恐怖酣倾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谤专,我是刑警寧澤躁锡,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站置侍,受9級特大地震影響映之,放射性物質(zhì)發(fā)生泄漏拦焚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一杠输、第九天 我趴在偏房一處隱蔽的房頂上張望赎败。 院中可真熱鬧,春花似錦蠢甲、人聲如沸僵刮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搞糕。三九已至,卻和暖如春曼追,著一層夾襖步出監(jiān)牢的瞬間寞宫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工拉鹃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留辈赋,地道東北人。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓膏燕,卻偏偏與公主長得像钥屈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子坝辫,可洞房花燭夜當晚...
    茶點故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容