[SpringBoot源码分析二]:@Condition

1. 背景介绍

在整个SpringBoot项目中关于Bean的注册,我们可能需要指示Bean只有在所有条件满足的情况下才有资格注册到容器中,比如说像下面这个例子,如果说我们已经注册过ViewResolver自然不需要SpringMVC帮我们注册了

上面的ConditionalOnBean正是Conditional的一种延伸

本文将围绕着SpringBoot中如何利用Conditional及其子注解进行条件判断来讲解

2. @Conditional

我们先看看这个注解的样子,发现这个接口其实就是传入一个Condition类作为检查类

java 复制代码
...
public @interface Conditional {

   /**
    * 检查类
    */
   Class<? extends Condition>[] value();

}

Condition也很简单,就只有一个matches方法,根据返回值判断是否匹配成功

java 复制代码
@FunctionalInterface
public interface Condition {

   boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

@ConditionalCondition长的非常像,事实上呢这两也是一一对应的,比如说@ConditionalOnBean对应的就是OnBeanCondition

SpringBoot中支持的@Conditional注解非常的多,接下来我会逐一讲解下面的注解

3. ConditionEvaluator

这里为什么要介绍这个类呢,前面我只是介绍了这些注解的作用以及对应的检查类,但是SpringBoot是如何判断一个Bean需要执行检查的呢,又是怎么判断需要通过哪个检查类去检查呢? 没错靠ConditionEvaluator

紧接着我们看ConditionEvaluator是在哪里执行的,我这里只举一个例子,当我们的候选配置类都被包装为ConfigurationClass然后转为BeanDefinitions的时候会执行loadBeanDefinitionsForConfigurationClass(...)方法, 大家可以看到这里首当其冲的进行了检查

大家这里不用纠结什么ConfigurationClassBeanDefinitions,只需要记住Bean在生命周期会通过ConditionEvaluator去确定Bean需要检查什么类型

接着我们看ConditionEvaluator的核心方法shouldSkip(...)

java 复制代码
public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
   // 没有携带 Conditional 的注解不需要跳过
   if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
      return false;
   }

   if (phase == null) {
      //如果是注解元数据,并且标志了某些注解
      if (metadata instanceof AnnotationMetadata &&
            ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
         //再递归调用:看是否需要跳过
         return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
      }
      //再递归调用:REGISTER_BEAN:表示无论条件是否成立都加入
      return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
   }

   List<Condition> conditions = new ArrayList<>();
   //获取当前类上有关@Conditional注解的信息
   //是一个List<String[]>,list表示可能有多个@Conditional注解
   for (String[] conditionClasses : getConditionClasses(metadata)) {
      //其中String[]是因为@Conditional是可以传入多个Class,其他的衍生类,就不行了,指定了一个了
      for (String conditionClass : conditionClasses) {
         //将当前类所标志的@Conditional类型的注解实例化,加入候选集合中
         Condition condition = getCondition(conditionClass, this.context.getClassLoader());
         conditions.add(condition);
      }
   }

   //是利用AnnotationAwareOrderComparator进行排序,
   //这是一个针对Ordered接口、PriorityOrdered接口和@Order注解进行排序的
   AnnotationAwareOrderComparator.sort(conditions);

   //遍历所有的@Conditional注解,看能否满足条件
   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;
      }
   }

   return false;
}

虽然说这个方法很长,但是当我精简下,大家就明白了:

  • 如若元数据没有带有@Conditional及其子注解就直接返回
  • 一旦发现了,就把上面带有的检查类拿出来,进行实例化
  • 将所有检查类进行排序,其排序逻辑正是检查类上带有的@Order注解
  • 然后调用检测类的matches(...)方法进行检查,如果有任何一个检查类匹配失败,则认为需要跳过

这里对部分@Condition子注解和对应的检查类做一个归类,方便大家理解

  • @ConditionalOnBeanOnBeanCondition
  • @ConditionalOnClassOnClassCondition
  • @ConditionalOnWebApplicationOnWebApplicationCondition
  • ....

4. @ConditionalOnBean

@ConditionalOnBean:要求容器中必须存在某种Bean,其检查类是OnBeanCondition

在介绍OnBeanCondition之前呢,我们得先了解下这个类的结构

我们可以发现它实现了两个重要的接口

  • SpringBootCondition
  • FilteringSpringBootCondition

我们先看SpringBootCondition,它实现了Condition接口,本质上就是基类,定义了matches(...)方法,最终的匹配结果其实是getMatchOutcome(...)方法决定的

java 复制代码
public abstract class SpringBootCondition implements Condition {
   
   ...
   @Override
   public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
      // 获取全路径(包名 + 类名)
      String classOrMethodName = getClassOrMethodName(metadata);
      try {
         // 获得匹配结果
         ConditionOutcome outcome = getMatchOutcome(context, metadata);
         logOutcome(classOrMethodName, outcome);
         // 记录条件评估的发生情况
         recordEvaluation(context, classOrMethodName, outcome);
         // 返回匹配结果
         return outcome.isMatch();
      }
      catch (NoClassDefFoundError ex) {
        ...
      }
      catch (RuntimeException ex) {
         throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
      }
   }
   ...
}

紧接着我们看FilteringSpringBootCondition类,大家细心的话就会发现这个类还实现了AutoConfigurationImportFilter接口,大家看名字就知道这是一个关于自动配置类的匹配类

java 复制代码
abstract class FilteringSpringBootCondition extends SpringBootCondition
      implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {
   ...
   /**
    * 返回自动配置类评估结果
    * @param autoConfigurationClasses 自动配置候选类
    * @param autoConfigurationMetadata 自动配置类规则
    * @return
    */
   @Override
   public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
      ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory);
      // 获得评估结果, 某个元素为空代表匹配成功
      ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
      boolean[] match = new boolean[outcomes.length];
      for (int i = 0; i < outcomes.length; i++) {
         match[i] = (outcomes[i] == null || outcomes[i].isMatch());
         // 匹配失败的情况
         if (!match[i] && outcomes[i] != null) {
            logOutcome(autoConfigurationClasses[i], outcomes[i]);
            if (report != null) {
               // 记录条件评估的发生情况
               report.recordConditionEvaluation(autoConfigurationClasses[i], this, outcomes[i]);
            }
         }
      }
      return match;
   }
}

如果大家看过我的写的[SpringBoot源码分析一]:@SpringBootApplication,就知道这个方法的入参是什么了

至于为什么要多定义一个match方法呢,我理解如下:

  • 方法的入参不一样
  • 由于spring.factoriesspring-autoconfigure-metadata.properties文件中可能存在多个自动配置类以及规则,那么返回的类型肯定需要是一个数组或者集合了

刚才我说了matches(...)的匹配结果是靠getMatchOutcome(...)方法决定的,所以我们直接看这个方法在OnBeanCondition中的实现

java 复制代码
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
   ConditionMessage matchMessage = ConditionMessage.empty();
   MergedAnnotations annotations = metadata.getAnnotations();
   //  检查 ConditionalOnBean 情况
   if (annotations.isPresent(ConditionalOnBean.class)) {
      Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
      // 返回匹配结果
      MatchResult matchResult = getMatchingBeans(context, spec);
      // 无法匹配成功
      if (!matchResult.isAllMatched()) {
         // 创建无法匹配成功的原因
         String reason = createOnBeanNoMatchReason(matchResult);
         return ConditionOutcome.noMatch(spec.message().because(reason));
      }
      // 到这就说明条件成立,封装返回消息
      matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
            matchResult.getNamesOfAllMatches());
   }

   //  检查 ConditionalOnSingleCandidate 情况
   if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
      Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
      // 返回匹配结果
      MatchResult matchResult = getMatchingBeans(context, spec);
      // 先判断是否有此Bean
      if (!matchResult.isAllMatched()) {
         return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
      }
      // 再看是否只存在一个候选Bean,或者只有一个主要候选Bean
      else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
            spec.getStrategy() == SearchStrategy.ALL)) {
         return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
               .items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
      }
      // 到这就说明条件成立,封装返回消息
      matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE,
            matchResult.getNamesOfAllMatches());
   }

   // 检查 ConditionalOnMissingBean 情况
   if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
      Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
            ConditionalOnMissingBean.class);
      // 返回匹配结果
      MatchResult matchResult = getMatchingBeans(context, spec);
      // 如果匹配成功,在这个注解的情况下,表示不能加入容器中
      if (matchResult.isAnyMatched()) {
         // 创建的 @OnMissingBean 的无法匹配结果
         String reason = createOnMissingBeanNoMatchReason(matchResult);
         // 返回不匹配的信息
         return ConditionOutcome.noMatch(spec.message().because(reason));
      }
      matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
   }
   // 返回匹配成功的信息
   return ConditionOutcome.match(matchMessage);
}

虽然说这个方法看起来很长,其实很简单,由于OnBeanCondition这个检查类是支持多个注解的@ConditionalOnBean@ConditionalOnSingleCandidate@ConditionalOnMissingBean,所以这里封装为了Spec类,然就调用了getMatchingBeans(...)方法

然后我们看了getMatchingBeans(...)方法,其实就是针对Bean类型、Bean名称,注解进行匹配

java 复制代码
protected final MatchResult getMatchingBeans(ConditionContext context, Spec<?> spec) {
   ClassLoader classLoader = context.getClassLoader();
   ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
   boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
   Set<Class<?>> parameterizedContainers = spec.getParameterizedContainers();
   // 是否在所有父容器中查找
   if (spec.getStrategy() == SearchStrategy.ANCESTORS) {
      BeanFactory parent = beanFactory.getParentBeanFactory();
      Assert.isInstanceOf(ConfigurableListableBeanFactory.class, parent,
            "Unable to use SearchStrategy.ANCESTORS");
      beanFactory = (ConfigurableListableBeanFactory) parent;
   }
   MatchResult result = new MatchResult();
   // 获得所有可以忽略的Bean名称
   Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(classLoader, beanFactory, considerHierarchy,
         spec.getIgnoredTypes(), parameterizedContainers);

   // 类型的匹配
   for (String type : spec.getTypes()) {
      // 获得指定类型Bean的名称
      Collection<String> typeMatches = getBeanNamesForType(classLoader, considerHierarchy, beanFactory, type,
            parameterizedContainers);
      // 移除需要忽略的
      typeMatches.removeAll(beansIgnoredByType);

      // 记录匹配不成功或者成功
      if (typeMatches.isEmpty()) {
         result.recordUnmatchedType(type);
      }
      else {
         result.recordMatchedType(type, typeMatches);
      }
   }

   // 注解的匹配,和类型的匹配差不多
   for (String annotation : spec.getAnnotations()) {
      Set<String> annotationMatches = getBeanNamesForAnnotation(classLoader, beanFactory, annotation,
            considerHierarchy);
      annotationMatches.removeAll(beansIgnoredByType);
      if (annotationMatches.isEmpty()) {
         result.recordUnmatchedAnnotation(annotation);
      }
      else {
         result.recordMatchedAnnotation(annotation, annotationMatches);
      }
   }

   // 名称的匹配,和类型的匹配差不多
   for (String beanName : spec.getNames()) {
      if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {
         result.recordMatchedName(beanName);
      }
      else {
         result.recordUnmatchedName(beanName);
      }
   }
   return result;
}

5. @ConditionalOnClass

@ConditionalOnClass:检查类是OnClassCondition,要求能够通过ClassLoader加载到指定的类

java 复制代码
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
   ClassLoader classLoader = context.getClassLoader();
   ConditionMessage matchMessage = ConditionMessage.empty();

   // ConditionalOnClass的情况
   List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
   if (onClasses != null) {
      // 对传入的类进行过滤,返回不能加载的类
      List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
      // 匹配失败:有无法加载的类
      if (!missing.isEmpty()) {
         return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
               .didNotFind("required class", "required classes").items(Style.QUOTE, missing));
      }
      // 匹配成功
      matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
            .found("required class", "required classes")
            .items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
   }

   // ConditionalOnMissingClass的情况
   List<String> onMissingClasses = getCandidates(metadata, ConditionalOnMissingClass.class);
   if (onMissingClasses != null) {
      // 对传入的类进行过滤,返回能加载的类
      List<String> present = filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);
      // 匹配失败:有可以加载的类
      if (!present.isEmpty()) {
         return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class)
               .found("unwanted class", "unwanted classes").items(Style.QUOTE, present));
      }
      // 匹配成功
      matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
            .didNotFind("unwanted class", "unwanted classes")
            .items(Style.QUOTE, filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));
   }
   return ConditionOutcome.match(matchMessage);
}

其实这个类没什么好讲的,但是这个类实现了FilteringSpringBootCondition, 这里针对于自动配置类的过滤,做了特殊的设置,那么紧接着就来看看

大家看我的注释就知道,这里可以通过两个线程进行检查自动配置类

java 复制代码
protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
      AutoConfigurationMetadata autoConfigurationMetadata) {
   //如果系统内核大于1,就多开一个线程进行匹配
   if (Runtime.getRuntime().availableProcessors() > 1) {
      return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
   }
   //一个线程进行匹配
   else {
      OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0,
            autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());
      return outcomesResolver.resolveOutcomes();
   }
}

resolveOutcomesThreaded(...)方法中会将自动配置类分为两部分,分别交给了StandardOutcomesResolverThreadedOutcomesResolver去处理

java 复制代码
private ConditionOutcome[] resolveOutcomesThreaded(String[] autoConfigurationClasses,
      AutoConfigurationMetadata autoConfigurationMetadata) {
   // 确定第一个线程的结束位置和第二个线程的开始位置
   int split = autoConfigurationClasses.length / 2;
   // 创建一个多线程的匹配类
   OutcomesResolver firstHalfResolver = createOutcomesResolver(autoConfigurationClasses, 0, split,
         autoConfigurationMetadata);
   // 创建第二个用当前线程的匹配类
   OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(autoConfigurationClasses, split,
         autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());

   // 开始匹配
   ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();
   ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes();

   // 封账匹配结果,并返回
   ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
   System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length);
   System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length);
   return outcomes;
}

StandardOutcomesResolver其实就很简单就for循环检查,而ThreadedOutcomesResolver就会利用多线程去进行匹配

6. 其他注解

至于其他的注解其实不太常见,我这就就简单做个介绍

  • @OnExpressionCondition:支持以SpEL表达式去解析
  • @ConditionalOnJava:要求JVM版本范围的
  • @ConditionalOnWebApplication:要求当前Web程序类型,是Servlet还是Reactive
  • ...
相关推荐
用户908324602732 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840823 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解3 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解3 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记3 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者4 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840824 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解4 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者5 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺5 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端