Spring 条件注解 @Conditional 使用及其底层实现

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 概述@Conditional 是 Spring 4.0 提出的一个新的注解,可以用在类或方法上,当标注的对象满足所有的条件时,才能注册为 Spring 中的 bean。条件由使用 Spring 的用户自己指定,例如指定的 bean 不存在时注册、不同的环境注册不同的bean 等。

概述


@Conditional 是 Spring 4.0 提出的一个新的注解,可以用在类或方法上,当标注的对象满足所有的条件时,才能注册为 Spring 中的 bean。条件由使用 Spring 的用户自己指定,例如指定的 bean 不存在时注册、不同的环境注册不同的bean 等。事实上 SpringBoot 中也大量的使用了 @Conditional 注解,并且将常用的条件抽象为 @ConditionalOnXXX 。


@Conditional 定义


先看 @Conditional 源码中的定义。


@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
  Class<? extends Condition>[] value();
}


可以看到 @Conditional 可以使用在类上或者方法上。具体使用方式如下。


在标注或元标注了 @Component 组件的类上进行标注。

作为元注解直接标注在其他注解上面。

在标注了 @Bean 注解的方法上标注。

@Conditional 注解上有一个 value 属性,其值只能为 Condition 类型的数组,使用 @Conditional 时必须进行指定。Condition 是指具体的条件,条件是否匹配由 Condition 进行控制,其是一个接口,需要我们自己实现。查看其源码如下。


@FunctionalInterface
public interface Condition {
  /**
   * 确定条件是否匹配
   */
  boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}


Condition 是一个函数式接口,只有一个 matches 方法,返回 true 表示 Component 可以注册为 Spring 中的 bean。有两个参数,ConditionContext 表示条件的上下文信息,AnnotatedTypeMetadata 表示类或方法上的注解元信息。先看 ConditionContext ,源码如下。


public interface ConditionContext {
  /**
   * 获取持有 BeanDefinition 的注册中心,BeanDefinition 是 Spring bean 的定义,BeanDefinitionRegistry 可以用来注册或查找 BeanDefinition。
   */
  BeanDefinitionRegistry getRegistry();
  /**
   * 获取 BeanFactory,BeanFactory 是 spring 最基础的容器,保存有 spring 的 bean 实例
   */
  @Nullable
  ConfigurableListableBeanFactory getBeanFactory();
  /**
   * 获取当前应用运行的环境信息
   */
  Environment getEnvironment();
  /**
   * 获取当前使用的 ResourceLoader,其可以用来加载各种各样的资源
   */
  ResourceLoader getResourceLoader();
  /**
   * 获取加载其他类的 ClassLoader
   */
  @Nullable
  ClassLoader getClassLoader();
}


在判断条件是否匹配时,可以使用的各种上下文信息都可以通过 ConditionContext 获取。而 AnnotatedTypeMetadata 表示 @Conditional 注解的类或方法上的注解元数据。只要获取注解信息就可以使用它。简单分析其源码如下。


public interface AnnotatedTypeMetadata {
  /**
   * 返回直接标注的注解详情
   * @since 5.2
   */
  MergedAnnotations getAnnotations();
  /**
   * 确定元素是否具有给定类型定义的注解或元注解
   */
  default boolean isAnnotated(String annotationName) {
    return getAnnotations().isPresent(annotationName);
  }
  /**
   * 获取给定类型的注解属性,考虑属性重写
   */
  @Nullable
  default Map<String, Object> getAnnotationAttributes(String annotationName) {
    return getAnnotationAttributes(annotationName, false);
  }
  /**
   * 获取给定类型的注解属性,考虑属性重写
   */
  @Nullable
  default Map<String, Object> getAnnotationAttributes(String annotationName,
      boolean classValuesAsString) {
    MergedAnnotation<Annotation> annotation = getAnnotations().get(annotationName,
        null, MergedAnnotationSelectors.firstDirectlyDeclared());
    if (!annotation.isPresent()) {
      return null;
    }
    return annotation.asAnnotationAttributes(Adapt.values(classValuesAsString, true));
  }
  /**
   * 获取给定类型的所有注解的所有属性,不考虑属性重写
   */
  @Nullable
  default MultiValueMap<String, Object> getAllAnnotationAttributes(String annotationName) {
    return getAllAnnotationAttributes(annotationName, false);
  }
  /**
   * 获取给定类型的所有注解的所有属性,不考虑属性重写
   */
  @Nullable
  default MultiValueMap<String, Object> getAllAnnotationAttributes(
      String annotationName, boolean classValuesAsString) {
    Adapt[] adaptations = Adapt.values(classValuesAsString, true);
    return getAnnotations().stream(annotationName)
        .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
        .map(MergedAnnotation::withNonMergedAttributes)
        .collect(MergedAnnotationCollectors.toMultiValueMap(map ->
            map.isEmpty() ? null : map, adaptations));
  }
}


@Conditional 执行时机


注解驱动的 Spring 编程模型中,注解主要包括两大块,其中一块是使用在配置类上,另一块是使用在配置类的 @Bean 标注的方法上。配置类是指存在注解 @Componet、@Import、@ImportResource、@ComponentScan 或方法上存在 @Bean 的类,参见org.springframework.context.annotation.ConfigurationClassUtils#isConfigurationCandidate。应用中通常会指定一个 Spring 扫描 bean 的包名,Spring 将包中满足条件的类识别为配置类,然后对配置类进行解析,解析的结果可能包含新的配置类,Spring 反复处理,直到没有新的配置类。然后将所有的配置类注册为 Spring 中的 bean。@Conditional 的执行时机就包含在解析配置类和注册 bean 两大阶段。


如果我们希望在解析配置类时不进行 @Conditional 判断,在注册 bean 时再进行 @Conditional 判断,则需要使用 Condition 的子类 ConfigurationCondition 。 ConfigurationCondition 源码如下。


public interface ConfigurationCondition extends Condition {
  /**
   * 返回条件被评估的阶段
   */
  ConfigurationPhase getConfigurationPhase();
  /**
   * 条件可以被评估的各种配置阶段
   */
  enum ConfigurationPhase {
    /**
     * Condition 应该在 @Configuration 类在解析时进行评估
     */
    PARSE_CONFIGURATION,
    /**
     * Condition 应该在 bean 注册时评估
     */
    REGISTER_BEAN
  }
}


@Conditional 使用示例


需求为一个 bean 必须在另一个 bean 存在时才进行注册,实现代码如下。


/**
 * @author zzuhkp
 * @date 2020-09-02 10:07
 * @since 1.0
 */
public class Application {
    public static class ConditionOnBeforeService implements ConfigurationCondition {
        @Override
        public ConfigurationPhase getConfigurationPhase() {
            return ConfigurationPhase.REGISTER_BEAN;
        }
        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return context.getBeanFactory().containsBean("beforeService");
        }
    }
    public static class BeforeService{
    }
    public static class AfterService{
    }
    @Configuration
    public static class BeforeConfig {
        @Bean
        public BeforeService beforeService() {
            return new BeforeService();
        }
    }
    @Conditional(ConditionOnBeforeService.class)
    @Configuration
    public static class AfterConfig {
        @Bean
        public AfterService afterService() {
            return new AfterService();
        }
    }
    @Import({BeforeConfig.class, AfterConfig.class})
    public static class Config {
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        System.out.println(context.getBean("afterService"));
    }
}


AfterService 需要在 BeforeService 注册为 bean 时才能注册为bean,这里使用的 ConfigurationPhase 为 REGISTER_BEAN,这是因为 BeforeService 在 BeforeConfig 中定义,如果不指定或指定为 PARSE_CONFIGURATION,则 Spring 在解析配置类 AfterConfig 时,BeforeService 还未注册为 spring 中的 bean。值得注意的是需要保证 Spring bean 的注册顺序,如果 BeforeService 不能保证在前面注册,AfterService 也不会注册到 spring。如果把 Config 改成下面,调换 BeforeConfig 和 AfterConfig 的顺序,Spring 由于找不到 AfterService 将会报错。


    @Import({AfterConfig.class,BeforeConfig.class})
    public static class Config {
    }


@Conditional 实现分析


Spring 扫描 bean 的流程在前面的文章 (Spring 类路径下 Bean 扫描实现分析 ) 中有进行介绍。Spring 扫描出所有的 bean 之后会循环对 bean 进行处理,解析配置类。核心代码位置为 org.springframework.context.annotation.ConfigurationClassParser#processConfigurationClass,部分代码如下。


  protected void processConfigurationClass(ConfigurationClass configClass, Predicate<String> filter)
      throws IOException {
    if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
      return;
    }
    ...省略部分代码
  }


解析完配置类之后会对配置类包含的所有 bean 进行注册,代码位置为org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass,部分代码如下。


  private void loadBeanDefinitionsForConfigurationClass(
      ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
    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());
      // 配置类 @Conditional 不满足条件,不再注册配置类包含的其他 bean
      return;
    }
    ... 省略部分代码
  }


这个方法使用了 TrackedConditionEvaluator 进行条件判断。TrackedConditionEvaluator 最终会调用 ConditionEvaluator#shouldSkip 方法。也就是说不管解析配置类还是注册 bean 都会使用到方法 ConditionEvaluator#shouldSkip ,查询其源码如下。


class ConditionEvaluator {
  private final ConditionContextImpl context;
  public ConditionEvaluator(@Nullable BeanDefinitionRegistry registry,
      @Nullable Environment environment, @Nullable ResourceLoader resourceLoader) {
    this.context = new ConditionContextImpl(registry, environment, resourceLoader);
  }
  public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
    if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
      // 如果不存在 @Conditional 注解,则跳过处理
      return false;
    }
    if (phase == null) {
      if (metadata instanceof AnnotationMetadata &&
          ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
        // 默认为解析配置类阶段
        return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
      }
      return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
    }
    List<Condition> conditions = new ArrayList<>();
    // 根据元信息获取 Condition 实例
    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) {
        // 获取 @Conditional 指定的处理阶段
        requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
      }
      if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
        // 没有指定处理阶段或需要处理的阶段与 @Conditional 指定的阶段相同,并且任意一个 Condition 不满足条件则跳过对 bean 定义的处理
        return true;
      }
    }
    return false;
  }
}


ConditionEvaluator 实例化时创建 ConditionContext 上下文,判断条件是否成立时根据 @Conditional 注解获取到 Condition 实例列表,而且还会对 Condition 进行排序。所有的 Condition 都满足条件时 bean 才会被 Spring 处理。并且还对 ConfigurationPhase 进行了处理,处理组件时的阶段和需要处理的阶段一致才进行判断。


总结

@Conditional 注解可以用在配置类或 @Bean 方法上,所有条件都满足 Spring 才会继续处理 bean。

@Conditional 执行阶段包括配置类的解析和 bean 的注册两大阶段。

通过 ConfigurationCondition 指定条件评估时的阶段。

@Conditional 中的多个 Condition 是并的关系,多个条件需要都满足。默认按照定义的顺序指定,可以通过 PriorityOrdered、Ordered 或 @Order 对Condition 进行排序。



目录
相关文章
|
8天前
|
Java Spring
【Spring】方法注解@Bean,配置类扫描路径
@Bean方法注解,如何在同一个类下面定义多个Bean对象,配置扫描路径
132 73
|
3天前
|
Java Spring 容器
【SpringFramework】Spring IoC-基于注解的实现
本文主要记录基于Spring注解实现IoC容器和DI相关知识。
35 21
|
8天前
|
存储 Java Spring
【Spring】获取Bean对象需要哪些注解
@Conntroller,@Service,@Repository,@Component,@Configuration,关于Bean对象的五个常用注解
|
8天前
|
Java Spring
【Spring配置】idea编码格式导致注解汉字无法保存
问题一:对于同一个项目,我们在使用idea的过程中,使用汉字注解完后,再打开该项目,汉字变成乱码问题二:本来a项目中,汉字注解调试好了,没有乱码了,但是创建出来的新的项目,写的注解又成乱码了。
|
2月前
|
前端开发 Java Spring
Spring MVC核心:深入理解@RequestMapping注解
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的核心,它将HTTP请求映射到控制器的处理方法上。本文将深入探讨`@RequestMapping`注解的各个方面,包括其注解的使用方法、如何与Spring MVC的其他组件协同工作,以及在实际开发中的应用案例。
47 4
|
2月前
|
前端开发 Java 开发者
Spring MVC中的请求映射:@RequestMapping注解深度解析
在Spring MVC框架中,`@RequestMapping`注解是实现请求映射的关键,它将HTTP请求映射到相应的处理器方法上。本文将深入探讨`@RequestMapping`注解的工作原理、使用方法以及最佳实践,为开发者提供一份详尽的技术干货。
135 2
|
2月前
|
前端开发 Java Spring
探索Spring MVC:@Controller注解的全面解析
在Spring MVC框架中,`@Controller`注解是构建Web应用程序的基石之一。它不仅简化了控制器的定义,还提供了一种优雅的方式来处理HTTP请求。本文将全面解析`@Controller`注解,包括其定义、用法、以及在Spring MVC中的作用。
58 2
|
2月前
|
消息中间件 Java 数据库
解密Spring Boot:深入理解条件装配与条件注解
Spring Boot中的条件装配与条件注解提供了强大的工具,使得应用程序可以根据不同的条件动态装配Bean,从而实现灵活的配置和管理。通过合理使用这些条件注解,开发者可以根据实际需求动态调整应用的行为,提升代码的可维护性和可扩展性。希望本文能够帮助你深入理解Spring Boot中的条件装配与条件注解,在实际开发中更好地应用这些功能。
40 2
|
2月前
|
JSON Java 数据格式
springboot常用注解
@RestController :修饰类,该控制器会返回Json数据 @RequestMapping(“/path”) :修饰类,该控制器的请求路径 @Autowired : 修饰属性,按照类型进行依赖注入 @PathVariable : 修饰参数,将路径值映射到参数上 @ResponseBody :修饰方法,该方法会返回Json数据 @RequestBody(需要使用Post提交方式) :修饰参数,将Json数据封装到对应参数中 @Controller@Service@Compont: 将类注册到ioc容器
|
2月前
|
前端开发 Java 开发者
Spring MVC中的控制器:@Controller注解全解析
在Spring MVC框架中,`@Controller`注解是构建Web应用程序控制层的核心。它不仅简化了控制器的定义,还提供了灵活的请求映射和处理机制。本文将深入探讨`@Controller`注解的用法、特点以及在实际开发中的应用。
96 0