Spring 条件注解没生效?咋回事
条件注解相信各位小伙伴都用过,Spring 中的多环境配置 profile 底层就是通过条件注解来实现的,松哥在之前的 Spring 视频中也有和大家详细介绍过条件注解的使用,感兴趣的小伙伴戳这里:Spring源码应该怎么学?。
从 Spring4.0 开始,Spring 提供了一个更加细粒度的条件注解: ConfigurationCondition。从名字上就可以看出来这个是搭配 @Configuration 注解一起使用的,ConfigurationCondition 提供了一种更加细粒度的条件匹配,可以在配置或者 Bean 注册的时候去评估条件注解是否满足。
也就是说,当一个类上存在条件注解的时候,我们可以有两个评估条件注解是否满足的时机:
- 在配置的时候去评估。
- 在 Bean 注册的时候评估。
在配置的时候评估,可能会导致当前类都不会被加载,在 Bean 注册的时候再去评估,意味着当前类就会被加载。
1. ConfigurationCondition
我们先来看下这个类的定义:
public interface ConfigurationCondition extends Condition { ConfigurationPhase getConfigurationPhase(); enum ConfigurationPhase { PARSE_CONFIGURATION, REGISTER_BEAN } }
大家看到,这里其实就是定义了两个枚举值,然后提供了一个方法返回枚举值。
- PARSE_CONFIGURATION:这个表示 Condition 条件应该在解析 @Configuration 类时进行评估,如果评估不通过,则不会将 @Configuration 添加到容器中。
- REGISTER_BEAN:这个表示添加常规 Bean 的时候去评估 Condition 条件(常规 Bean 就是指非配置类,例如添加搭配 @Bean 注解使用的条件注解),这个条件不会阻止注册 @Configuration 类到容器中。
其实道理很好懂,就是加载配置类的时候就根据条件注解判断要不要加载配置类,还是等到注册 Bean 的时候再去看条件注解是否满足条件。
2. 案例分析
松哥通过一个简单案例来和小伙伴们演示一下。
假设我现在有如下条件:
public class MyCondition implements ConfigurationCondition { @Override public ConfigurationPhase getConfigurationPhase() { return ConfigurationPhase.PARSE_CONFIGURATION; } @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { return context.getBeanFactory().containsBean("a"); } }
这个条件我没有直接实现 Condition 接口,而是实现类 ConfigurationCondition 接口,在这个接口中,getConfigurationPhase 方法返回了 PARSE_CONFIGURATION,表示在加载配置类的时候就去评估条件是否满足,matches 方法则是去判断容器中是否存在一个名为 a 的 Bean。
现在我有两个配置类,分别是 A 和 B,如下:
@Configuration public class A { } @Configuration @Conditional(MyCondition.class) public class B { }
A 配置类正常加载,B 配置类有一个加载条件,就是得 A 存在,B 才会加载。
现在,在容器中加载 B 和 A 两个配置,如下:
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); ctx.register(B.class,A.class); ctx.refresh(); String[] beanDefinitionNames = ctx.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { System.out.println(beanDefinitionName); }
大家注意,加载的时候,我先加载了 B,后加载了 A,这点很重要,加载 B 的时候,由于此时容器中还不存在一个名为 a 的 Bean,而我们的评估时机是在处理配置类的时候,因此就会导致 B 配置类不会被加载,最终打印出来的 BeanName 就没有 b。
但是,如果我们将 MyCondition 中,条件注解的评估时机改为 ConfigurationPhase.REGISTER_BEAN
,那么就表示在系统启动的时候,并不会去评估条件注解是否满足,而是会将 @Configuration 配置类进行解析,此时启动系统,就会发现最终打印出来的 beanName 里既有 a 又有 b。
3. 源码分析
接下来我们再来从源码的角度来分析一下上述行为。
在 Spring 中,提供了一个专门的内部类 ConditionEvaluator 来处理要不要跳过条件注解,该类中有一个名为 shouldSkip 的方法,用来处理此事:
public boolean shouldSkip(AnnotatedTypeMetadata metadata) { return shouldSkip(metadata, null); } public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) { if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) { return false; } if (phase == null) { if (metadata instanceof AnnotationMetadata annotationMetadata && ConfigurationClassUtils.isConfigurationCandidate(annotationMetadata)) { return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION); } return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN); } List<condition> conditions = new ArrayList<>(); for (String[] conditionClasses : getConditionClasses(metadata)) { for (String conditionClass : conditionClasses) { Condition condition = getCondition(conditionClass, this.context.getClassLoader()); conditions.add(condition); } } AnnotationAwareOrderComparator.sort(conditions); for (Condition condition : conditions) { ConfigurationPhase requiredPhase = null; if (condition instanceof ConfigurationCondition configurationCondition) { requiredPhase = configurationCondition.getConfigurationPhase(); } if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) { return true; } } return false; }
第一个方法不用多说,我们来看第二个重载方法,重载方法多了一个参数 ConfigurationPhase,这个就表示配置的阶段,也就是条件注解生效的阶段。
首先会去判断当前注解是否是一个条件注解,如果不是条件注意,那么就不能跳过,要继续后面的解析(继续后面的解析时 Bean 将会被注册),如果是条件注解,则继续后面的判断。继续判断,如果没有传递 phase 进来,说明没有指定应该在哪个阶段去评估条件注解,那么这个时候就去判断,如果当前注解是一个配置类上的注解,那么就设置 phase 为 PARSE_CONFIGURATION,然后继续调用 shouldSkip 方法,否则就设置 phase 为 REGISTER_BEAN 然后继续调用 shouldSkip 方法。
> 那么什么样的情况会被认为是一个配置类上的注解呢?如果当前类上添加的注解时 @Component、@ComponentScan、@Import、@ImportResource 以及这四种注解衍生出来的注解,亦或者当前类中有 @Bean 注解标记的方法,那么当前类就是一个配置类,就会设置 phase 为 PARSE_CONFIGURATION。
第二次进入 shouldSkip 方法的时候,就已经有明确的 phase 了。这次进来后,把所有的条件注解的条件收集起来,存入到 conditions 集合中,然后再对该集合进行排序。然后遍历该集合。遍历的时候就去判断这个条件注解是不是 ConfigurationCondition 类型的,如果是,则提取出来其中的 phase 为 requiredPhase,这个就表示这个条件注意希望自己被处理的阶段,接下来去判断,如果 requiredPhase 为空,说明条件并未指定自己的执行时间,那么就执行 matches 方法进行条件评估;如果 requiredPhase 不为空,并且和传入的 phase 相等,那么也是当前评估。其实这个判断核心逻辑就是以参数传入进来的 phase 为准,要么条件没有设置评估时机,要么设置了,但是得和参数传进来的 phase 一致,只有满足这两个条件,才会当场进行评估。
这就是系统条件注解的评估逻辑。
对于配置类来说,是在 AnnotatedBeanDefinitionReader#doRegisterBean
方法中调用评估逻辑的:
private <t> void doRegisterBean(Class<t> beanClass, @Nullable String name, @Nullable Class<!--? extends Annotation-->[] qualifiers, @Nullable Supplier<t> supplier, @Nullable BeanDefinitionCustomizer[] customizers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { return; } //... }
调用的时候并未明确指定 phase,所以会在进入到 shouldSkip 方法后,自行分析是哪个阶段评估条件注解。
对于 @Bean 注解标记的类来说,是在 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod
方法中调用评估逻辑的:
private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { ConfigurationClass configClass = beanMethod.getConfigurationClass(); MethodMetadata metadata = beanMethod.getMetadata(); String methodName = metadata.getMethodName(); if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) { configClass.skippedBeanMethods.add(methodName); return; } //... }
这个调用的时候,就传入了 phase 了,直接指定了是在 Bean 初始化的时候评估。
好啦,这就是条件注解条件评估时机的两种情况。在 Spring Boot 中定义的条件注解里,有不少都用到了 ConfigurationCondition,而不是传统的 Condition,感兴趣的小伙伴可以自行查看哦~</t></t></t></condition>

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
PHP 最新统计数据:市场份额超 7 成、CMS 中的王者
Wikimedia 基金会首席工程师 Timo Tijhof 发表文章《An Internet of PHP》,探讨了 PHP 在互联网中的广泛应用和重要性。 下面是文章整理的部分数据。 PHP 仍然是首选编程语言 根据W3 Techs 对全球前 1000 万个网站使用的编程语言分析(截至 2023.8): PHP 占比 77.2% ASP 占比 6.9% Ruby 占比 5.4% 基于 PHP 的内容管理框架 绝大多数公开网站都是使用基于 PHP 的 CMS 进行构建。根据市场份额,12 大 CMS 软件中有 8 个 采用 PHP 编写。 下面的数据来自W3 Techs 对前 1000 万个网站的 CMS 使用情况调查,每个百分点代表前 1000 万个网站中的 10 万网站。 [PHP] WordPress 生态 (63%) [Ruby] Shopify Wix Squarespace [PHP] Joomla 生态 (3%) [PHP] Drupal 生态 (2%) [PHP] Adobe Magento (2%) [PHP] PrestaShop (1%) [Python] Go...
- 下一篇
请谨慎使用 @Builder 注解!
一、前言 前一段时间写过一篇 《使用 lombok @Builder 注解,设置默认值时要千万小心!》 的文章,文章提到使用 @Builder 注解将会导致设置的默认值失效问题。 最近读了一篇文章:《Oh !! Stop using @Builder》[1]也颇受启发,发现很多人的确被 @Builder 注解的名字误导了。 大多数同学使用 @Builder 无非就是为了链式编程,然而 @Builder 并不是链式编程的最佳实践,它会额外创建内部类,存在继承关系时还需要使用 @SuperBuilder 注解[2],设置默认值时也需要额外的 @Builder.Default 去设置默认值[3],无疑增加了很多不必要的复杂度。 有些同学可能说如果你把这些问题都了解就不会遇到这些坑,没必要“因噎废食”,可是这些问题已经让无数人一次次趟坑,说明它并不是最佳实践,如果有更简便办法可以实现链式编程,又何必徒增这么多复杂度呢?其实很多人选择使用 @Builder 实现链式编程,无非是它“更常见”和“最熟悉” ! 二、为什么? (1)@Builder 生成的构造器不是完美的,它不能区分哪些参数是必须的,...
相关文章
文章评论
共有0条评论来说两句吧...