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