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 中被引入,用于開(kāi)發(fā) “If-Then-Else” 類型的 bean 注冊(cè)條件檢查。在 @Conditional 之前,也有一個(gè)注解 @Porfile 起到類似的作用,它們兩個(gè)的區(qū)別在于:
- @Profile 僅用于基于環(huán)境變量的條件檢查,使用范圍比較窄。
- @Conditional 更加通用,開(kāi)發(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)注在其他的注解上面,用于編寫(xiě)自定義注解
- 作為任何 @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 注解就寫(xiě)好了,使用時(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 注解用于開(kāi)發(fā) “If-Then-Else” 類型的 bean 注冊(cè)條件檢查。
- @Conditional 可以使用在類或方法上,具體的用法有三種:
- 作為類注解,標(biāo)注在直接或間接使用了 @Component 的類上,包括 @Configuration 類
- 作為元注解,直接標(biāo)注在其他的注解上面,用于編寫(xiě)自定義注解
- 作為任何 @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)文章
淺談Java數(shù)值類型的轉(zhuǎn)換與強(qiáng)制轉(zhuǎn)換
這篇文章主要介紹了Java數(shù)值類型的轉(zhuǎn)換與強(qiáng)制轉(zhuǎn)換,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
SpringCloud之Feign代理,聲明式服務(wù)調(diào)用方式
這篇文章主要介紹了SpringCloud之Feign代理,聲明式服務(wù)調(diào)用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03
java實(shí)現(xiàn)多線程的兩種方式繼承Thread類和實(shí)現(xiàn)Runnable接口的方法
下面小編就為大家?guī)?lái)一篇java實(shí)現(xiàn)多線程的兩種方式繼承Thread類和實(shí)現(xiàn)Runnable接口的方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09
Spring的@Scheduled 如何動(dòng)態(tài)更新cron表達(dá)式
這篇文章主要介紹了Spring的@Scheduled 如何動(dòng)態(tài)更新cron表達(dá)式的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java并發(fā)編程之Condition源碼分析(推薦)
這篇文章主要介紹了Java并發(fā)編程之Condition源碼分析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03

