Spring 从 3.0 开始支持条件装配,@Conditional 和 @Configuration 可以说构成了 Spring 注解驱动的核心,也是 Spring Boot 自动装配的基础。本文会以一道阿里的面试题[1]引出相关的话题:

  1. @Conditional 是做什么的?
  2. @Conditional 多个条件是什么逻辑关系?
  3. 条件判断在什么时候执行?
  4. ConfigurationCondition 和 Condition 有什么区别?什么时候使用 ConfigurationCondition?
  5. 多个 Condition 执行的顺序是什么样的?可以配置优先级么?
  6. 可以介绍一下 @Conditional 常见的一些用法么?

    1. @Conditional 简介

    @Conditional 是 Spring 提供的条件装配的核心 API,在 Spring Framework 中感受还并不明显,但在 Spring Boot 中大行其道。与 @Conditional 相关的 API 有:
  • @Conditional:条件装配的核心注解。Spring Boot 基于这个标准标注派生出多个子注解,如 @ConditionalOnBean、@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnWebApplication 等。
  • Condition:定义 @Conditional 注解的匹配规则。
  • ConfigurationCondition:相比于 Condition 接口,多出 getConfigurationPhase() 方法,可以获取 @Conditional 的装配阶段,默认配置解析阶段和Bean注入阶段都会进行匹配。
  • ConditionEvaluator:用于处理 @Conditional 注解。提供了 shouldSkip 方法,判断该 Bean 是否需要注入。

    1.1 相关概念

    @Conditional 注解会指定条件配置的规则 Condition。如果有多个匹配规则,则先排序后匹配。如果没有定义顺序,则按配置的顺序逐一匹配。
    1. @Target({ElementType.TYPE, ElementType.METHOD})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. public @interface Conditional {
    4. Class<? extends Condition>[] value();
    5. }

    1.1.1 @Conditional 用法

    @Conditional 大致有以下两种用法:
  1. 标注在上:需要经历解析和注册两个阶段,通常配合 @Configuration 使用。因为标注在类上,需要被 @ComponentScan 扫描到后注册到容器中,因此类上一般至少标注有 @Componen 或其派生注解。配置类解析时,先判断是否解析这个配置类;配置类注册时,判断这个配置类是否需要注册。
  2. 标注在方法上:只会经历注册阶段,通常配合 @Bean 使用。

说明:如果 @Conditional 标注在配置类上,而这个 Condition 条件只有配置类解析后才能满足。为了让配置类被解析,需要让 @Conditional 跳过配置阶段,这就是 ConfigurationCondition 存在的意义。

  1. @Configuration
  2. @ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) // 类上使用,配合@Configuration
  3. @EnableConfigurationProperties(JdbcProperties.class)
  4. public class JdbcTemplateAutoConfiguration {
  5. @Bean
  6. @ConditionalOnMissingBean(JdbcOperations.class) // 方法上使用,配合@Bean
  7. public JdbcTemplate jdbcTemplate() {
  8. ...
  9. }
  10. }

1.1.2 Spring 配置类

Spring 配置类有狭义和广义之分。@Configuration 是狭义配置类,其它的配置类则是广义配置类。具体见 ConfigurationClassUtils#isConfigurationCandidate

  1. 类上有 @Configuration 注解
  2. 类上有 @Compontent 注解
  3. 类上有 @CompontentScan 注解
  4. 类上有 @Import 注解
  5. 类上有 @ImportResource 注解
  6. 类中有 @Bean 标注的方法

    1.2 核心 API

    1.2.1 Condition

    1. public interface Condition {
    2. boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
    3. }
    说明:Condition 接口中 matches 方法有两个参数:
  • ConditionContext:条件装配的上下文环境,封装了 BeanFactory 和 Environment 等上下文环境,用于 Condition 判断是否需要注入。
  • AnnotatedTypeMetadata:这是 Spring Core 中处理注解的核心 API,封装了 @Conditional 标注的类或方法的全部注解信息,包括 @Conditional 注解和其它的所有注解。

比如,@Profile 就是使用 ProfileCondition 实现的。

  1. @Conditional(ProfileCondition.class)
  2. public @interface Profile {
  3. String[] value();
  4. }
  5. class ProfileCondition implements Condition {
  6. @Override
  7. public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  8. MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
  9. if (attrs != null) {
  10. for (Object value : attrs.get("value")) {
  11. if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
  12. return true;
  13. }
  14. }
  15. return false;
  16. }
  17. return true;
  18. }
  19. }

1.2.2 ConfigurationCondition

ConfigurationCondition 的理解相对来说难一点,在 Spring Boot 中大量使用。

  1. public interface ConfigurationCondition extends Condition {
  2. ConfigurationPhase getConfigurationPhase();
  3. enum ConfigurationPhase {
  4. PARSE_CONFIGURATION, // 1. 配置解析阶段。如果不匹配,不会解析配置文件,但配置类会注册到容器中
  5. REGISTER_BEAN // 2. Bean注册阶段。如果汪匹配,不会注册到容器中
  6. }
  7. }

说明:Spring 将配置解析具体分为两个阶段:

  1. PARSE_CONFIGURATION:配置解析阶段。如果匹配失败,不会解析配置文件。
  2. REGISTER_BEAN:注册阶段。如果匹配失败,不会注册到容器中。

    2. 源码分析

    @Conditional 是 ConditionEvaluator 处理,提供了 shouldSkip 方法,来判断是否忽略该 Bean。@Conditional 通常是和 @Cofiguration 一起处理,分为解析和注册两个阶段,其中 ConfigurationClassPostProcessor 处理解析阶段,ConfigurationClassBeanDefinitionReader 处理注册阶段。
  • ConditionEvaluator:(核心)封装了对 @Conditional 的处理逻辑。
  • ConfigurationClassPostProcessor:(核心)解析阶段。配置类是否解析。
  • ConfigurationClassBeanDefinitionReader:(核心)注册阶段。是否注册。
  • AnnotatedBeanDefinitionReader:是否注册。处理 @Conditional 装配条件。
  • ClassPathScanningCandidateComponentProvider:是否注册。处理 @Conditional 装配条件。

    2.1 ConditionEvaluator

    ConditionEvaluator 会读取 @Conditional 注解的装配条件 Condition,调用 shouldSkip 方法进行条件匹配。
  1. Condition:不管是解析还是注册阶段都会进行条件匹配,返回调用 Condition#matches 匹配的结果。
  2. ConfigurationCondition:相比于 Condition,增加了配置阶段的匹配。当装配阶段不一致时,直接返回 false,也就是需要解析或注册该 Bean。

    1. public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    2. // 1. 没有@Conditional注解,直接过
    3. if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
    4. return false;
    5. }
    6. // 2. 如果直接调用shouldSkip(metadata)方法,则根据该类是否是配置类决定装配阶段
    7. if (phase == null) {
    8. if (metadata instanceof AnnotationMetadata &&
    9. ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
    10. return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
    11. }
    12. return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
    13. }
    14. // 3. 获取Conditon,并排序
    15. List<Condition> conditions = new ArrayList<>();
    16. for (String[] conditionClasses : getConditionClasses(metadata)) {
    17. for (String conditionClass : conditionClasses) {
    18. Condition condition = getCondition(conditionClass, this.context.getClassLoader());
    19. conditions.add(condition);
    20. }
    21. }
    22. AnnotationAwareOrderComparator.sort(conditions);
    23. // 4. (核心)如果不是ConfigurationCondition,则无论是解析还是注册阶段都会进行匹配
    24. for (Condition condition : conditions) {
    25. ConfigurationPhase requiredPhase = null;
    26. if (condition instanceof ConfigurationCondition) {
    27. requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
    28. }
    29. if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
    30. return true;
    31. }
    32. }
    33. return false;
    34. }

    2.2 ConfigurationClassPostProcessor

    ConfigurationClassPostProcessor 是 @Configuration 注解的处理入口,调用 processConfigBeanDefinitions 方法解析配置类。ConfigurationClassPostProcessor 解析配置类,ConfigurationClassBeanDefinitionReader 则用于注册。 Spring 注解驱动(06)@Conditional - 图1说明:ConfigurationClassPostProcessor 中可以看到配置类都会经历解析和注册两个阶段:

  3. 解析阶段:ConfigurationClassParser 解析是 PARSE_CONFIGURATION 阶段。

  4. 注册阶段:ConfigurationClassBeanDefinitionReader 会将 ConfigurationClassParser 解析的配置信息注册到容器中,因此是 REGISTER_BEAN 阶段。另外,需要注意的是,如果配置类不能注册,则其解析的相关配置也不会注册到容器中。
    1. // ConfigurationClassBeanDefinitionReader加载解析的配置信息
    2. private void loadBeanDefinitionsForConfigurationClass(
    3. ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
    4. // 配置类不能注册,则直接返回。不会注册其解析的配置信息
    5. if (trackedConditionEvaluator.shouldSkip(configClass)) {
    6. return;
    7. }
    8. // 注册解析的配置信息
    9. registerBeanDefinitionForImportedConfigurationClass(configClass);
    10. loadBeanDefinitionsForBeanMethod(beanMethod);
    11. loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
    12. loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
    13. }

    3. 使用示例

    ```java @Configuration @Conditional(MyCondition1.class) public class BeanAutoConfiguration1 { @Bean public HelloService helloService() {
    1. return new helloServiceImpl();
    } }

public class MyCondition1 implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { //获取spring容器 ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); //判断容器中是否存在Service类型的bean boolean existsService = !beanFactory.getBeansOfType(HelloService.class).isEmpty(); return existsService; } } ```

总结时刻

推荐阅读

  1. @Conditional通过条件来控制bean的注册》:强烈推荐。

每天用心记录一点点。内容也许不重要,但习惯很重要!