Spring中的@Conditional注解使用和原理詳解
前言
熟悉 SpringBoot 的小伙伴們肯定不會(huì)對(duì) @Conditional 注解感到陌生,它在 SpringBoot 的自動(dòng)化配置特性中起到了非常重要的作用。
許多配置類在加載 Bean 時(shí)都使用到了 @ConditionalOnClass、@ConditionalOnBean,@ConditionalOnProperty 等 @Conditional 的衍生注解。
那么,在單純的 Spring 項(xiàng)目中,我們是否也可以使用 @Conditional 來(lái)實(shí)現(xiàn)一些自動(dòng)化配置的特性呢?
我們?cè)撛趺礃尤ナ褂聾Conditional? 它又是如何生效的?
別著急,本篇文章會(huì)一一解答。
概述
@Conditional 在 Spring 4.0 中被引入,用于開發(fā) “If-Then-Else” 類型的 bean 注冊(cè)條件檢查。在 @Conditional 之前,也有一個(gè)注解 @Porfile 起到類似的作用,它們兩個(gè)的區(qū)別在于:
- @Profile 僅用于基于環(huán)境變量的條件檢查,使用范圍比較窄。
- @Conditional 更加通用,開發(fā)人員可以自定義條件檢查策略??捎糜?bean 注冊(cè)時(shí)的條件檢查。
- 4.3.8后,@Profile 也基于 @Conditional 來(lái)實(shí)現(xiàn)。
用法
首先來(lái)看一下源碼中 @Conditional 的定義
package org.springframework.context.annotation; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { Class<? extends Condition>[] value(); }
根據(jù)定義, @Conditional 可以使用在類或方法上,具體的用法有:
- 作為類注解,標(biāo)注在直接或間接使用了 @Component 的類上,包括 @Configuration 類
- 作為元注解,直接標(biāo)注在其他的注解上面,用于編寫自定義注解
- 作為任何 @Bean 方法的方法級(jí)注解
@Conditional 有一個(gè)屬性 value,其類型是 Condition 數(shù)組。組件必須匹配數(shù)組中所有的 Condition,才可以被注冊(cè)。
package org.springframework.context.annotation; @FunctionalInterface public interface Condition { /** * 判斷條件是否匹配 * @param context 上下文信息,可以從中獲取 BeanDefinitionRegistry,BeanFactory,Environment,ResourceLoader,ClassLoader 等一些用于資源加載的信息 * @param metadata 注解的元信息,可以從中獲取注解的屬性 * @return {@code true} 條件匹配,組件可以注冊(cè) * or {@code false} 否決組件的注冊(cè) */ boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
Condition 是一個(gè)函數(shù)式接口,只有一個(gè) matches 方法,返回 true 則表示條件匹配。matches 方法的兩個(gè)參數(shù)分別是上下文信息和注解的元信息,從這兩個(gè)參數(shù)中可以獲取到 IOC 容器和當(dāng)前組件的信息,從而判斷條件是否匹配。 由于 ConditionContext 和 AnnotatedTypeMetadata 的方法都比較簡(jiǎn)單,這里就不貼出源碼了,有興趣的小伙伴可自行翻看源碼。 Condition 必須遵循與 BeanFactoryPostProcessor 相同的限制,并注意永遠(yuǎn)不要與 bean 實(shí)例交互。如果要對(duì)與 @Configuration bean 交互的條件進(jìn)行更細(xì)粒度的控制,可以考慮 ConfigurationCondition 接口。
public interface ConfigurationCondition extends Condition { /** * 返回條件生效的階段 */ ConfigurationPhase getConfigurationPhase(); enum ConfigurationPhase { /** * 在 @Configuration 類解析時(shí)生效 */ PARSE_CONFIGURATION, /** * 在 bean 注冊(cè)時(shí)生效。此時(shí)所有的 @Configuration 都解析完成了。 */ REGISTER_BEAN } }
接下來(lái)我們?cè)?Spring 下實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 ConditionalOnBean 注解,實(shí)現(xiàn)一個(gè) bean 只有在另一個(gè) bean 存在時(shí),才進(jìn)行注冊(cè)。
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented // Conditional 作為元注解,主要的判斷邏輯在 OnBeanCondition 類中 @Conditional(OnBeanCondition.class) public @interface ConditionalOnBean { // bean 的名稱 String[] name() default {}; } // OnBeanCondition 主要的判斷邏輯在 matches 方法中 class OnBeanCondition implements ConfigurationCondition { @Override public ConfigurationPhase getConfigurationPhase() { return ConfigurationPhase.REGISTER_BEAN; } @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { if (metadata.isAnnotated(ConditionalOnBean.class.getName())) { MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(ConditionalOnShardingProps.class.getName()); if (attrs != null) { ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); for (Object beanName : attrs.get("name")) { if(!beanFactory.containsBean((String) beanName)) { return false; } } return true; } } return true; } } // 使用 ConditionalOnBean 注解 @Configuration @Conditional(ConditionalOnBean.class) public static class OnBeanConfig { @Bean @ConditionalOnBean(name = "a") public B b() { return new B(); } }
這樣,一個(gè)自定義的 Conditional 注解就寫好了,使用時(shí)只要把它加到類或方法上即可生效。
原理
首先,通過(guò)調(diào)用鏈路的分析可知,Conditional 的調(diào)用方是 ConditionEvaluator,而 ConditionEvaluator 在 ConfigurationClassParser、ConfigurationClassBeanDefinitionReader 和 AnnotatedBeanDefinitionReader 中均有所使用。先來(lái)看下這三個(gè)類在 Spring 的流程中扮演什么角色。 ConfigurationClassParser ConfigurationClassParser 在 ConfigurationClassPostProcessor 中被使用到,而 ConfigurationClassPostProcessor 是一個(gè) BeanDefinitionRegistryPostProcessor,顧名思義,就是在 bean 掃描完成后,對(duì) bean 的定義進(jìn)行修改的一個(gè)后置處理器,主要的功能在于解析 bean 中的所有配置類。
// ConfigurationClassParser 的核心邏輯 protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { // 調(diào)用 shouldSkip 方法,對(duì)應(yīng)的階段是 PARSE_CONFIGURATION if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; } ... 省略 }
ConfigurationClassBeanDefinitionReader
ConfigurationClassBeanDefinitionReader 也是在 ConfigurationClassPostProcessor 中被使用到。在配置類解析完成后,對(duì)其中包含的 bean 進(jìn)行注冊(cè)。
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { // 調(diào)用的還是 conditionEvaluator.shouldSkip,在其基礎(chǔ)上做了個(gè)緩存。 // 對(duì)應(yīng)的階段是 REGISTER_BEAN if (trackedConditionEvaluator.shouldSkip(configClass)) { String beanName = configClass.getBeanName(); if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { this.registry.removeBeanDefinition(beanName); } this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName()); return; } ... 省略 }
AnnotatedBeanDefinitionReader
AnnotatedBeanDefinitionReader 主要在 AnnotationConfigApplicationContext 中被使用到。AnnotationConfigApplicationContext 是 Spring 中的一個(gè)高級(jí)容器,與 ClassPathXmlApplicationContext 不同的是,它主要通過(guò)解析 Java 配置文件中的配置,來(lái)進(jìn)行 bean 的注冊(cè)。
<T> void doRegisterBean(Class<T> beanClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name, @Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); // 調(diào)用 shouldSkip 方法,對(duì)應(yīng)的階段為 null if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } ... 省略 }
綜上,我們已經(jīng)了解了 ConditionEvaluator 在 Spring 的流程中是如何發(fā)揮作用的,接下來(lái)看看核心方法 shouldSkip 的具體實(shí)現(xiàn)邏輯。
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { // 不存在 Conditional 注解,則不處理 if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { return false; } // 階段為空時(shí)的處理邏輯 if (phase == null) { // 有 Configuration、Component、ComponentScan、Import、ImportResource 等注解,則任務(wù)是配置解析階段 if (metadata instanceof AnnotationMetadata && ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } // 獲取所有 Conditional 注解,并提取出 Condition 類 List<Condition> conditions = new ArrayList<>(); for (String[] conditionClasses : getConditionClasses(metadata)) { for (String conditionClass : conditionClasses) { Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); } } // 對(duì) Condition 進(jìn)行排序 AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } // 調(diào)用 Condition 的 matches 方法,不符合條件的則跳過(guò) if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } // 所有的 Condition 都符合,則不跳過(guò),進(jìn)行后續(xù)處理 return false; }
看了源代碼,相信小伙伴們對(duì) Conditional 的理解又深入了一層。
- Conditional 可以用作元注解加在自定義注解之上。
- Spring 在解析配置類或者注冊(cè) bean 時(shí),都會(huì)調(diào)用 ConditionEvaluator#shouldSkip 方法,判斷是否符合注冊(cè)條件。
- shouldSkip 會(huì)獲取到組件上的所有 Conditional 注解,并拿到注解上的所有 Condition 類,調(diào)用 Condition#matches 進(jìn)行判斷。
- Condition 默認(rèn)按照定義的順序來(lái)執(zhí)行,一般通過(guò) @Order 對(duì)Condition 進(jìn)行排序。
- 只有所有條件都符合,Spring 才會(huì)進(jìn)行后續(xù)的處理流程。
總結(jié)
- @Conditional 注解用于開發(fā) “If-Then-Else” 類型的 bean 注冊(cè)條件檢查。
- @Conditional 可以使用在類或方法上,具體的用法有三種:
- 作為類注解,標(biāo)注在直接或間接使用了 @Component 的類上,包括 @Configuration 類
- 作為元注解,直接標(biāo)注在其他的注解上面,用于編寫自定義注解
- 作為任何 @Bean 方法的方法級(jí)注解
- @Conditional 在解析配置類和注冊(cè) bean 這兩個(gè)階段生效??梢酝ㄟ^(guò) ConfigurationCondition 指定階段。
- Condition 默認(rèn)按照定義的順序來(lái)執(zhí)行,一般通過(guò) @Order 對(duì)Condition 進(jìn)行排序。
- 組件必須匹配所有的 Condition,才可以被注冊(cè)。
到此這篇關(guān)于Spring中的@Conditional注解使用和原理詳解的文章就介紹到這了,更多相關(guān)Spring中的@Conditional注解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot實(shí)現(xiàn)調(diào)用百度ocr實(shí)現(xiàn)身份識(shí)別+二要素校驗(yàn)功能
本文介紹了如何使用Spring Boot調(diào)用百度OCR服務(wù)進(jìn)行身份識(shí)別,并通過(guò)二要素校驗(yàn)確保信息準(zhǔn)確性,感興趣的朋友一起看看吧2025-03-03使用Java實(shí)現(xiàn)獲取文件MD5值工具類
我們?cè)诠ぷ髦型ǔJ褂肕D5對(duì)文件進(jìn)行校驗(yàn)完整性,比較,提高安全性等,這篇文章主要為大家詳細(xì)介紹了Java如何編寫一個(gè)實(shí)現(xiàn)獲取文件MD5值的工具,需要的可以參考下2023-12-12mybatisplus報(bào)錯(cuò):Invalid bound statement(not fou
文章主要介紹了在使用MyBatis-Plus時(shí)遇到的`Invalid bound statement (not found)`錯(cuò)誤的幾種常見(jiàn)原因和解決方法,包括namespace路徑不一致、函數(shù)名或標(biāo)簽id不一致、構(gòu)建未成功、掃包配置錯(cuò)誤以及配置文件書寫錯(cuò)誤2025-02-02Java利用JSch實(shí)現(xiàn)SSH遠(yuǎn)程操作的技術(shù)指南
在日常開發(fā)中,許多應(yīng)用需要通過(guò) SSH 協(xié)議遠(yuǎn)程連接服務(wù)器來(lái)執(zhí)行命令、上傳或下載文件,JSch是一個(gè)功能強(qiáng)大的 Java 庫(kù),它提供了便捷的接口來(lái)實(shí)現(xiàn) SSH 連接和其他遠(yuǎn)程管理功能,本文將介紹 JSch 的基本功能,并通過(guò)實(shí)際代碼示例幫助您快速上手,需要的朋友可以參考下2025-03-03idea web項(xiàng)目沒(méi)有小藍(lán)點(diǎn)的的兩種解決方法
本文主要介紹了idea web項(xiàng)目沒(méi)有小藍(lán)點(diǎn)的的兩種解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07