Spring中的@Conditional注解使用和原理详解
前言
熟悉 SpringBoot 的小伙伴们肯定不会对 @Conditional 注解感到陌生,它在 SpringBoot 的自动化配置特性中起到了非常重要的作用。
许多配置类在加载 Bean 时都使用到了 @ConditionalOnClass、@ConditionalOnBean,@ConditionalOnProperty 等 @Conditional 的衍生注解。
那么,在单纯的 Spring 项目中,我们是否也可以使用 @Conditional 来实现一些自动化配置的特性呢?
我们该怎么样去使用@Conditional? 它又是如何生效的?
别着急,本篇文章会一一解答。
概述
@Conditional 在 Spring 4.0 中被引入,用于开发 “If-Then-Else” 类型的 bean 注册条件检查。在 @Conditional 之前,也有一个注解 @Porfile 起到类似的作用,它们两个的区别在于:
- @Profile 仅用于基于环境变量的条件检查,使用范围比较窄。
- @Conditional 更加通用,开发人员可以自定义条件检查策略。可用于 bean 注册时的条件检查。
- 4.3.8后,@Profile 也基于 @Conditional 来实现。
用法
首先来看一下源码中 @Conditional 的定义
package org.springframework.context.annotation; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Conditional { Class<? extends Condition>[] value(); }
根据定义, @Conditional 可以使用在类或方法上,具体的用法有:
- 作为类注解,标注在直接或间接使用了 @Component 的类上,包括 @Configuration 类
- 作为元注解,直接标注在其他的注解上面,用于编写自定义注解
- 作为任何 @Bean 方法的方法级注解
@Conditional 有一个属性 value,其类型是 Condition 数组。组件必须匹配数组中所有的 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 是一个函数式接口,只有一个 matches 方法,返回 true 则表示条件匹配。matches 方法的两个参数分别是上下文信息和注解的元信息,从这两个参数中可以获取到 IOC 容器和当前组件的信息,从而判断条件是否匹配。 由于 ConditionContext 和 AnnotatedTypeMetadata 的方法都比较简单,这里就不贴出源码了,有兴趣的小伙伴可自行翻看源码。 Condition 必须遵循与 BeanFactoryPostProcessor 相同的限制,并注意永远不要与 bean 实例交互。如果要对与 @Configuration bean 交互的条件进行更细粒度的控制,可以考虑 ConfigurationCondition 接口。
public interface ConfigurationCondition extends Condition { /** * 返回条件生效的阶段 */ ConfigurationPhase getConfigurationPhase(); enum ConfigurationPhase { /** * 在 @Configuration 类解析时生效 */ PARSE_CONFIGURATION, /** * 在 bean 注册时生效。此时所有的 @Configuration 都解析完成了。 */ REGISTER_BEAN } }
接下来我们在 Spring 下实现一个简单的 ConditionalOnBean 注解,实现一个 bean 只有在另一个 bean 存在时,才进行注册。
@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 注解就写好了,使用时只要把它加到类或方法上即可生效。
原理
首先,通过调用链路的分析可知,Conditional 的调用方是 ConditionEvaluator,而 ConditionEvaluator 在 ConfigurationClassParser、ConfigurationClassBeanDefinitionReader 和 AnnotatedBeanDefinitionReader 中均有所使用。先来看下这三个类在 Spring 的流程中扮演什么角色。 ConfigurationClassParser ConfigurationClassParser 在 ConfigurationClassPostProcessor 中被使用到,而 ConfigurationClassPostProcessor 是一个 BeanDefinitionRegistryPostProcessor,顾名思义,就是在 bean 扫描完成后,对 bean 的定义进行修改的一个后置处理器,主要的功能在于解析 bean 中的所有配置类。
// ConfigurationClassParser 的核心逻辑 protected void processConfigurationClass(ConfigurationClass configClass) throws IOException { // 调用 shouldSkip 方法,对应的阶段是 PARSE_CONFIGURATION if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return; } ... 省略 }
ConfigurationClassBeanDefinitionReader
ConfigurationClassBeanDefinitionReader 也是在 ConfigurationClassPostProcessor 中被使用到。在配置类解析完成后,对其中包含的 bean 进行注册。
private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) { // 调用的还是 conditionEvaluator.shouldSkip,在其基础上做了个缓存。 // 对应的阶段是 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 配置文件中的配置,来进行 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); // 调用 shouldSkip 方法,对应的阶段为 null if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } ... 省略 }
综上,我们已经了解了 ConditionEvaluator 在 Spring 的流程中是如何发挥作用的,接下来看看核心方法 shouldSkip 的具体实现逻辑。
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 等注解,则任务是配置解析阶段 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 进行排序 AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition) { requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase(); } // 调用 Condition 的 matches 方法,不符合条件的则跳过 if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } // 所有的 Condition 都符合,则不跳过,进行后续处理 return false; }
看了源代码,相信小伙伴们对 Conditional 的理解又深入了一层。
- Conditional 可以用作元注解加在自定义注解之上。
- Spring 在解析配置类或者注册 bean 时,都会调用 ConditionEvaluator#shouldSkip 方法,判断是否符合注册条件。
- shouldSkip 会获取到组件上的所有 Conditional 注解,并拿到注解上的所有 Condition 类,调用 Condition#matches 进行判断。
- Condition 默认按照定义的顺序来执行,一般通过 @Order 对Condition 进行排序。
- 只有所有条件都符合,Spring 才会进行后续的处理流程。
总结
- @Conditional 注解用于开发 “If-Then-Else” 类型的 bean 注册条件检查。
- @Conditional 可以使用在类或方法上,具体的用法有三种:
- 作为类注解,标注在直接或间接使用了 @Component 的类上,包括 @Configuration 类
- 作为元注解,直接标注在其他的注解上面,用于编写自定义注解
- 作为任何 @Bean 方法的方法级注解
- @Conditional 在解析配置类和注册 bean 这两个阶段生效。可以通过 ConfigurationCondition 指定阶段。
- Condition 默认按照定义的顺序来执行,一般通过 @Order 对Condition 进行排序。
- 组件必须匹配所有的 Condition,才可以被注册。
到此这篇关于Spring中的@Conditional注解使用和原理详解的文章就介绍到这了,更多相关Spring中的@Conditional注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Maven打包没有指定主类问题(xxx.jar中没有主清单属性)
这篇文章主要介绍了Maven打包没有指定主类问题(xxx.jar中没有主清单属性),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-04-04
最新评论