深度解析@Conditional注解
一、學(xué)習(xí)指引
Spring是如何根據(jù)條件創(chuàng)建Bean的?
日常工作過程中,相信這種情況是最常見的:根據(jù)某個或某些條件來執(zhí)行相應(yīng)的邏輯。換句話說,會通過if-else語句來執(zhí)行一定的業(yè)務(wù)邏輯功能。
在Spring中,就有這樣一個注解,它支持根據(jù)一定的條件來創(chuàng)建對應(yīng)的Bean對象,并將Bean對象注冊到IOC容器中。滿足條件的Bean就會被注冊到IOC容器中,不滿足條件的Bean就不會被注冊到IOC容器中。這個注解就是@Conditional注解,本章,就對@Conditional注解進(jìn)行簡單的介紹。
二、注解說明
關(guān)于@Conditional注解的一點(diǎn)點(diǎn)說明~~
Spring提供的@Conditional注解支持按照條件向IOC容器中注冊Bean,滿足條件的Bean就會被注冊到IOC容器中,不滿足條件的Bean就不會被注冊到IOC容器中。
2.1 注解源碼
@Conditional注解可以標(biāo)注到類或方法上,能夠?qū)崿F(xiàn)按照條件向IOC容器中注冊Bean。源碼詳見:org.springframework.context.annotation.Conditional。
從@Conditional注解的源碼可以看出,@Conditional注解是從Spring 4.0版本開始提供的注解。在@Conditional注解注解中只提供了一個Class數(shù)組類型的value屬性,具體含義如下所示。
- value:指定Condition接口的實(shí)現(xiàn)類,Condition接口的實(shí)現(xiàn)類中需要編寫具體代碼實(shí)現(xiàn)向Spring中注入Bean的條件。
2.2 使用場景
如果使用Spring開發(fā)的應(yīng)用程序需要根據(jù)不同的運(yùn)行環(huán)境來讀取不同的配置信息,例如在Windows操作系統(tǒng)上需要讀取Windows操作系統(tǒng)的環(huán)境信息,在MacOS操作系統(tǒng)上需要讀取MacOS操作系統(tǒng)的環(huán)境信息。此時(shí),就可以使用@Conditional注解實(shí)現(xiàn)。
另外,@Conditional注解還有如下一些使用場景:
- 可以作為類級別的注解直接或者間接的與@Component相關(guān)聯(lián),包括@Configuration類。
- 可以作為元注解,用于自動編寫構(gòu)造性注解。
- 作為方法級別的注解,作用在任何@Bean的方法上。
三、使用案例
@Conditional注解案例實(shí)戰(zhàn)~~
Spring的@Conditional注解可以標(biāo)注到類或方法上,并且會實(shí)現(xiàn)按照一定的條件將對應(yīng)的Bean注入到IOC容器中。所以,本節(jié),會列舉無條件(不加@Conditional注解)、@Conditional注解標(biāo)注到方法上和@Conditional注解標(biāo)注到類上以及將@Conditional注解同時(shí)標(biāo)注到類上和方法上等四個主要案例。
3.1 無條件案例
本節(jié),主要實(shí)現(xiàn)不使用@Conditional注解時(shí),向IOC容器中注入Bean的案例,具體實(shí)現(xiàn)步驟如下所示。
(1)新增Founder類
Founder類的源碼詳見:spring-annotation-chapter-08工程下的io.binghe.spring.annotation.chapter08.bean.Founder。
可以看到,F(xiàn)ounder類就是Java中的一個普通實(shí)體類。
(2)新增ConditionalConfig類
ConditionalConfig類的源碼詳見:spring-annotation-chapter-08工程下的io.binghe.spring.annotation.chapter08.config.ConditionalConfig。
可以看到,ConditionalConfig類是一個Spring的配置類,并且在ConditionalConfig類中使用@Bean注解創(chuàng)建了兩個Bean對象,并注冊到IOC容器中,一個Bean的名稱為bill,另一個Bean的名稱為jobs。
(3)新增ConditionalTest類
ConditionalTest類的源碼詳見:spring-annotation-chapter-08工程下的io.binghe.spring.annotation.chapter08.ConditionalTest。
可以看到,在ConditionalTest類的main()方法中,會打印注入到IOC容器中的Bean名稱。
(4)運(yùn)行ConditionalTest類
運(yùn)行ConditionalTest類的main()方法,輸出的結(jié)果信息如下所示。
從輸出的結(jié)果信息可以看出,向IOC容器中注入了名稱為conditionalConfig、bill和jobs的Bean。
說明:沒設(shè)置@Conditional注解時(shí),會向Spring容器中注入所有使用@Bean注解創(chuàng)建的Bean。
3.2 標(biāo)注到方法上的案例
本節(jié),主要實(shí)現(xiàn)將@Conditional注解標(biāo)注到方法上,向IOC容器中注入Bean的案例,具體實(shí)現(xiàn)步驟如下所示。
注意:本節(jié)的案例是在3.1節(jié)的基礎(chǔ)上進(jìn)行完善,在對應(yīng)的方法上添加@Conditional注解。
(1)新增WindowsCondition類
WindowsCondition類的源碼詳見:spring-annotation-chapter-08工程下的io.binghe.spring.annotation.chapter08.condition.WindowsCondition。
可以看到,WindowsCondition類實(shí)現(xiàn)了Condition接口,并實(shí)現(xiàn)了matches()方法。在matches()方法中,通過Spring的環(huán)境變量讀取操作系統(tǒng)名稱,如果操作系統(tǒng)名稱中包含windows就返回true,否則返回false。當(dāng)返回true時(shí),使用@Conditional注解指定的條件為WindowsCondition類的Class對象的Bean會被創(chuàng)建并注入到IOC容器中。
(2)新增MacosCondition類
MacosCondition類的源碼詳見:spring-annotation-chapter-08工程下的io.binghe.spring.annotation.chapter08.condition.MacosCondition。
可以看到,MacosCondition類實(shí)現(xiàn)了Condition接口,并實(shí)現(xiàn)了matches()方法。在matches()方法中,通過Spring的環(huán)境變量讀取操作系統(tǒng)名稱,如果操作系統(tǒng)名稱中包含mac就返回true,否則返回false。當(dāng)返回true時(shí),使用@Conditional注解指定的條件為MacosCondition類的Class對象的Bean會被創(chuàng)建并注入到IOC容器中。
(3)修改ConditionalConfig類
在ConditionalConfig類的方法上標(biāo)注@Conditional注解,修改后的源碼如下所示。
可以看到,在創(chuàng)建名稱為bill的Bean的方法上標(biāo)注了@Conditional注解,并指定了value的屬性為WindowsCondition類的class對象。在創(chuàng)建名稱為jobs的Bean的方法上標(biāo)注了@Conditional注解,并指定了value的屬性為MacosCondition類的class對象。
(4)運(yùn)行ConditionalTest類
運(yùn)行ConditionalTest類的main()方法,輸出的結(jié)果信息如下所示。
可以看到,由于我的電腦是Windows操作系統(tǒng),所以,打印出的Bean名稱包含conditionalConfig和bill,不包含jobs。
說明:@Conditional注解標(biāo)注到使用@Bean創(chuàng)建Bean的方法上時(shí),只有滿足@Conditional注解的條件時(shí),才會執(zhí)行方法體創(chuàng)建Bean對象并注入到IOC容器中。
3.3 標(biāo)注到類上的案例
本節(jié),主要實(shí)現(xiàn)將@Conditional注解標(biāo)注到類上,向IOC容器中注入Bean的案例,具體實(shí)現(xiàn)步驟如下所示。
注意:本節(jié)的案例是在3.1節(jié)的基礎(chǔ)上進(jìn)行完善,在對應(yīng)的類上添加@Conditional注解。
(1)修改ConditionalConfig類
刪除ConditionalConfig類中的方法上的@Conditional注解,并在ConditionalConfig類上標(biāo)注@Conditional注解。
可以看到,在ConditionalConfig類上標(biāo)注了@Conditional注解,并且將value屬性設(shè)置為MacosCondition。也就是說,當(dāng)前操作系統(tǒng)為MacOS操作系統(tǒng)時(shí),才會創(chuàng)建名稱為bill和jobs的Bean,并將其注入到IOC容器中。
(2)運(yùn)行ConditionalTest類
運(yùn)行ConditionalTest類的main()方法,輸出的結(jié)果信息如下所示。
從輸出的結(jié)果信息可以看出,由于我的電腦是Windows操作系統(tǒng),所以,在輸出的Bean名稱中,并不包含conditionalConfig、bill和jobs。
說明:當(dāng)@Conditional注解標(biāo)注到類上時(shí),如果運(yùn)行程序時(shí),不滿足@Conditional注解中指定的條件,則當(dāng)前類的所有Bean都不會被創(chuàng)建,也不會注入到IOC容器中。
3.4 同時(shí)標(biāo)注到類和方法上
本節(jié),主要實(shí)現(xiàn)將@Conditional注解同時(shí)標(biāo)注到類和方法上,向IOC容器中注入Bean的案例,具體實(shí)現(xiàn)步驟如下所示。
注意:本節(jié)的案例是在3.1節(jié)的基礎(chǔ)上進(jìn)行完善,在對應(yīng)的類上添加@Conditional注解。
(1)修改ConditionalConfig類
在ConditionalConfig類的類上和方法上同時(shí)標(biāo)注@Conditional注解。如下所示。
可以看到,在ConditionalConfig類的類上和方法上都標(biāo)注了@Conditional注解。其中,在類上標(biāo)注的@Conditional注解的條件是當(dāng)前操作系統(tǒng)為MacOS系統(tǒng)。
(2)運(yùn)行ConditionalTest類
運(yùn)行ConditionalTest類的main()方法,輸出的結(jié)果信息如下所示。
從輸出的結(jié)果信息可以看出,由于我的電腦是Windows操作系統(tǒng),所以,在輸出的Bean名稱中,并不包含conditionalConfig、bill和jobs。
說明:當(dāng)@Conditional注解同時(shí)標(biāo)注到類和方法上時(shí),如果標(biāo)注到類上的@Conditional注解不滿足條件,即使類中的方法上標(biāo)注的@Conditional注解滿足條件,也不會創(chuàng)建Bean,并且也不會將Bean注入到IOC容器中。
四、源碼時(shí)序圖
結(jié)合時(shí)序圖理解源碼會事半功倍,你覺得呢?
本節(jié),就以源碼時(shí)序圖的方式,直觀的感受下@Conditional注解在Spring源碼層面的執(zhí)行流程。@Conditional注解的源碼時(shí)序圖如圖8-1和8-2所示。
五、源碼解析
源碼時(shí)序圖整清楚了,那就整源碼解析唄!
@Conditional注解在Spring源碼層面的執(zhí)行流程,結(jié)合源碼執(zhí)行的時(shí)序圖,會理解的更加深刻。本節(jié),就簡單結(jié)合源碼時(shí)序圖簡單分析下@Conditional注解在Spring源碼層面的執(zhí)行流程。
注意:@Conditional注解在Spring源碼層面的執(zhí)行流程與第7章的5.1節(jié)@DependsOn注解在Spring源碼層面注冊Bean的執(zhí)行流程大體類似,只是在執(zhí)行AnnotatedBeanDefinitionReader類的doRegisterBean()方法的邏輯時(shí),略有差異。
(1)解析AnnotatedBeanDefinitionReader類的doRegisterBean(ClassbeanClass, String name, Class<? extends Annotation>[] qualifiers, Suppliersupplier, BeanDefinitionCustomizer[] customizers)方法
源碼詳見:org.springframework.context.annotation.AnnotatedBeanDefinitionReader#doRegisterBean(ClassbeanClass, String name, Class<? extends Annotation>[] qualifiers, Suppliersupplier, BeanDefinitionCustomizer[] customizers)。
可以看到,在AnnotatedBeanDefinitionReader類的doRegisterBean()方法中,調(diào)用了conditionEvaluator對象的shouldSkip()方法判斷是否要忽略當(dāng)前Bean的注冊。
(2)解析ConditionEvaluator類的shouldSkip(AnnotatedTypeMetadata metadata)方法
源碼詳見:org.springframework.context.annotation.ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata metadata)
可以看到,在ConditionEvaluator類的shouldSkip()方法中,直接調(diào)用了另一個重載的shouldSkip()方法。
(3)解析ConditionEvaluator類的shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase)方法
源碼詳見:org.springframework.context.annotation.ConditionEvaluator#shouldSkip(AnnotatedTypeMetadata metadata, ConfigurationPhase phase)。
可以看到,在shouldSkip()方法中,首先會判斷類或方法上是否標(biāo)注了@Conditional注解,如果沒有標(biāo)注@Conditional注解,則直接返回false,此時(shí)對應(yīng)的Bean會被創(chuàng)建并注入到IOC容器中。
否則,會解析@Conditional注解中的value屬性設(shè)置的Class對象,將Class對象的全類名解析到conditionClasses數(shù)組中,遍歷conditionClasses數(shù)組中的每個元素生成Condition對象,將Condition對象存入conditions集合中。后續(xù)會遍歷conditions集合中的每個Condition對象,調(diào)用matches()方法,此處的邏輯與matches()方法的返回值正好相反。
- matches()方法返回false,則此處返回true,表示對應(yīng)的Bean不會被創(chuàng)建,也不會注入到IOC容器中。
- matches()方法返回true,則此處返回false,表示對應(yīng)的Bean會被創(chuàng)建并且會注入到IOC容器中。
接下來,就會回到AnnotatedBeanDefinitionReader類的doRegisterBean()方法繼續(xù)執(zhí)行后續(xù)流程,后續(xù)流程與第7章的5.1節(jié)@DependsOn注解在Spring源碼層面注冊Bean的執(zhí)行流程一致,這里不再贅述。
至此,@Conditional注解在Spring源碼層面的執(zhí)行流程分析完畢。
六、擴(kuò)展注解
@Conditional的擴(kuò)展注解如下所示:
@ConditionalOnBean:僅僅在當(dāng)前上下文中存在某個對象時(shí),才會實(shí)例化一個Bean。@ConditionalOnClass:某個class位于類路徑上,才會實(shí)例化一個Bean。@ConditionalOnExpression:當(dāng)表達(dá)式為true的時(shí)候,才會實(shí)例化一個Bean。@ConditionalOnMissingBean:僅僅在當(dāng)前上下文中不存在某個對象時(shí),才會實(shí)例化一個Bean。@ConditionalOnMissingClass:某個class類路徑上不存在的時(shí)候,才會實(shí)例化一個Bean。@ConditionalOnNotWebApplication:不是web應(yīng)用,才會實(shí)例化一個Bean。@ConditionalOnBean:當(dāng)容器中有指定Bean的條件下進(jìn)行實(shí)例化。@ConditionalOnMissingBean:當(dāng)容器里沒有指定Bean的條件下進(jìn)行實(shí)例化。@ConditionalOnClass:當(dāng)classpath類路徑下有指定類的條件下進(jìn)行實(shí)例化。@ConditionalOnMissingClass:當(dāng)類路徑下沒有指定類的條件下進(jìn)行實(shí)例化。@ConditionalOnWebApplication:當(dāng)項(xiàng)目是一個Web項(xiàng)目時(shí)進(jìn)行實(shí)例化。@ConditionalOnNotWebApplication:當(dāng)項(xiàng)目不是一個Web項(xiàng)目時(shí)進(jìn)行實(shí)例化。@ConditionalOnProperty:當(dāng)指定的屬性有指定的值時(shí)進(jìn)行實(shí)例化。@ConditionalOnExpression:基于SpEL表達(dá)式的條件判斷。@ConditionalOnJava:當(dāng)JVM版本為指定的版本范圍時(shí)觸發(fā)實(shí)例化。@ConditionalOnResource:當(dāng)類路徑下有指定的資源時(shí)觸發(fā)實(shí)例化。@ConditionalOnJndi:在JNDI存在的條件下觸發(fā)實(shí)例化。@ConditionalOnSingleCandidate:當(dāng)指定的Bean在容器中只有一個,或者有多個但是指定了首選的Bean時(shí)觸發(fā)實(shí)例化。
七、總結(jié)
@Conditional注解介紹完了,我們一起總結(jié)下吧!
本章,首先介紹了@Conditional注解的源碼和使用場景。隨后,列舉了四個關(guān)于@Conditional注解的案例,分別是:無條件案例、標(biāo)注到方法上的案例、標(biāo)注到類上的案例和同時(shí)標(biāo)注到類和方法上的案例。接下來,介紹了@Conditional注解執(zhí)行的源碼時(shí)序圖和源碼流程。
八、思考
既然學(xué)完了,就開始思考幾個問題吧?
關(guān)于@Conditional注解,通常會有如下幾個經(jīng)典面試題:
- @Conditional注解的作用是什么?
- @Conditional注解有哪些使用場景?
- @Conditional注解與@Profile注解有什么區(qū)別?
- @Conditional注解在Spring內(nèi)層的執(zhí)行流程?
- 你在平時(shí)工作中,會在哪些場景下使用@Conditional注解?
- 你從@Conditional注解的設(shè)計(jì)中得到了哪些啟發(fā)?