《Spring Boot 高级》2.2 @Conditional 家族实现

从 Condition 接口、ConditionContext 到 @Conditional 的评估时机,拆解 OnClassCondition、OnBeanCondition、OnWebApplicationCondition 三者的实现思路:批量过滤、REGISTER_BEAN 阶段判断,以及评估结果如何被缓存并汇总进 --debug 报告。

本节目标:讲清 @Conditional 在启动流程的哪个阶段被求值、ConditionContext 能拿到什么,以及 OnClassCondition、OnBeanCondition、OnWebApplicationCondition 三者的实现差异与各自代价。
适用版本:Spring Boot 4.1.x(Java 21)

上一节看到,AutoConfigurationImportSelector 会在真正解析配置类之前,用三个 AutoConfigurationImportFilter 做一轮批量过滤。这三个 filter 同时也是普通的 Condition,本节就把它们拆开,回答一个常被误解的问题:条件到底在「解析阶段」还是「Bean 创建阶段」判断?

Condition 接口与 ConditionContext

条件的最小契约来自 Spring Framework(不是 Spring Boot):

export JAVA_HOME=/tmp/springboot_book/jdk-21.0.12.1+1/Contents/Home
SC=~/.m2/repository/org/springframework/spring-context/7.0.9/spring-context-7.0.9.jar
"$JAVA_HOME/bin/javap" -cp "$SC" org.springframework.context.annotation.Condition
public interface org.springframework.context.annotation.Condition {
  public abstract boolean matches(
      org.springframework.context.annotation.ConditionContext,
      org.springframework.core.type.AnnotatedTypeMetadata);
}

判断所需的全部上下文都从 ConditionContext 拿,它只暴露五个入口:

"$JAVA_HOME/bin/javap" -cp "$SC" org.springframework.context.annotation.ConditionContext
public interface org.springframework.context.annotation.ConditionContext {
  public abstract org.springframework.beans.factory.support.BeanDefinitionRegistry getRegistry();
  public abstract org.springframework.beans.factory.config.ConfigurableListableBeanFactory getBeanFactory();
  public abstract org.springframework.core.env.Environment getEnvironment();
  public abstract org.springframework.core.io.ResourceLoader getResourceLoader();
  public abstract java.lang.ClassLoader getClassLoader();
}

这五个入口对应了自动配置能问的五类问题:

入口能回答的问题对应条件
getClassLoader()classpath 上有没有某个类@ConditionalOnClass
getBeanFactory() / getRegistry()容器里有没有(或只有唯一一个)某个 Bean@ConditionalOnBean
getEnvironment()某个属性是什么值@ConditionalOnProperty
getResourceLoader()某个资源是否存在@ConditionalOnResource
以上组合当前是不是 Web 应用、JVM 版本、云平台@ConditionalOnWebApplication / @ConditionalOnJava / @ConditionalOnCloudPlatform

@Conditional 本身只是把 Condition 实现类挂上去的载体:

public interface org.springframework.context.annotation.Conditional extends java.lang.annotation.Annotation {
  public abstract java.lang.Class<? extends org.springframework.context.annotation.Condition>[] value();
}

Spring Boot 的 @ConditionalOnClass 等注解都是元注解:它们身上标着 @Conditional(OnClassCondition.class),所以最终求值的还是那批 Condition 实现。这一点决定了扩展方式——想加新条件,要么写一个新的 Condition + 注解,要么复用 AnyNestedCondition / AllNestedConditions / NoneNestedConditions 组合既有条件。

求值时机:解析阶段,不是 Bean 创建阶段

条件是在配置类解析阶段被求值的,由 ConditionEvaluator 驱动:

"$JAVA_HOME/bin/javap" -p -cp "$SC" org.springframework.context.annotation.ConditionEvaluator
class org.springframework.context.annotation.ConditionEvaluator {
  public org.springframework.context.annotation.ConditionEvaluator(
      org.springframework.beans.factory.support.BeanDefinitionRegistry,
      org.springframework.core.env.Environment,
      org.springframework.core.io.ResourceLoader);
  public boolean shouldSkip(org.springframework.core.type.AnnotatedTypeMetadata);
  public boolean shouldSkip(org.springframework.core.type.AnnotatedTypeMetadata,
      org.springframework.context.annotation.ConfigurationCondition$ConfigurationPhase);
  java.util.List<org.springframework.context.annotation.Condition> collectConditions(
      org.springframework.core.type.AnnotatedTypeMetadata);
}

ConfigurationClassPostProcessor → ConfigurationClassParser 在解析每个 @Configuration / @Component 时调用 shouldSkip(...),命中就整段跳过,对应的 Bean 定义根本不会注册,也就谈不上「创建 Bean 时再判断」。

由此引出一个经典陷阱:@ConditionalOnBean 只能看到在当前解析位置之前已经注册的 Bean 定义。如果你把 @ConditionalOnBean 写在一个被扫描顺序靠前的配置类上,而它依赖的 Bean 来自后面才解析的配置类,就会得到「条件不成立」的错误结论。

Spring 用 ConfigurationCondition 给条件留了第二次机会:

public interface org.springframework.context.annotation.ConfigurationCondition
    extends org.springframework.context.annotation.Condition {
  public abstract org.springframework.context.annotation.ConfigurationCondition$ConfigurationPhase getConfigurationPhase();
}

public enum ConfigurationCondition$ConfigurationPhase {
  PARSE_CONFIGURATION,
  REGISTER_BEAN
}
阶段何时求值谁用原因
PARSE_CONFIGURATION解析配置类时默认值,OnClassCondition 等只需 classpath / 环境信息,越早跳过越省事
REGISTER_BEANBean 定义注册时(更晚)OnBeanCondition依赖「容器里有哪些 Bean」,必须等到定义基本就绪

OnBeanCondition 的 getConfigurationPhase() 实测返回 REGISTER_BEAN(javap 常量池可见该字符串)。这就是它比 OnClassCondition 更晚、也更贵的原因。

SpringBootCondition:统一日志与批量能力

Spring Boot 的条件都继承自 SpringBootCondition:

AC=~/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/4.1.1/spring-boot-autoconfigure-4.1.1.jar
"$JAVA_HOME/bin/javap" -cp "$AC" org.springframework.boot.autoconfigure.condition.SpringBootCondition
public abstract class org.springframework.boot.autoconfigure.condition.SpringBootCondition
    implements org.springframework.context.annotation.Condition {
  public final boolean matches(ConditionContext, AnnotatedTypeMetadata);
  protected final void logOutcome(java.lang.String, ConditionOutcome);
  public abstract ConditionOutcome getMatchOutcome(ConditionContext, AnnotatedTypeMetadata);
  protected final boolean anyMatches(ConditionContext, AnnotatedTypeMetadata, Condition...);
  protected final boolean matches(ConditionContext, AnnotatedTypeMetadata, Condition);
}

设计要点:

  • matches 被声明为 final,模板方法把「记录评估结果」这一步固定下来:子类只实现 getMatchOutcome(...) 返回一个 ConditionOutcome,父类负责把它交给 ConditionEvaluationReport。这保证任何条件都会进入 --debug 报告,不会漏记。
  • 返回值从 boolean 升级为 ConditionOutcome——它带 isMatch() 和 getMessage()(如「required class X found / did not find」),这是报告里能写清「为什么没匹配」的基础。
  • logOutcome 会把结果写到 logger;因此 --debug 报告的来源正是所有 SpringBootCondition 子类的评估记录。

ConditionOutcome 的工厂方法(实测):

public static ConditionOutcome match();
public static ConditionOutcome match(java.lang.String);
public static ConditionOutcome noMatch(java.lang.String);
public boolean isMatch();
public java.lang.String getMessage();

批量过滤:FilteringSpringBootCondition

上一节的导入过滤要求对一批候选类同时判断,而 Condition.matches 一次只处理一个类。桥接两者的是:

abstract class FilteringSpringBootCondition extends SpringBootCondition
    implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {
  public boolean[] match(java.lang.String[], AutoConfigurationMetadata);
  protected abstract ConditionOutcome[] getOutcomes(java.lang.String[], AutoConfigurationMetadata);
  protected final java.util.List<java.lang.String> filter(Collection, ClassNameFilter, ClassLoader);
  protected static java.lang.Class<?> resolve(java.lang.String, ClassLoader);
}

它同时是 Condition(单个判断,走 getMatchOutcome)和 AutoConfigurationImportFilter(批量判断,走 getOutcomes)。批量接口返回 boolean[],与输入类名数组一一对应——容器据此在读取这些类的元数据之前就把不成立的候选剔除。这就是为什么 OnClassCondition、OnWebApplicationCondition 都同时实现了两个方法。

OnClassCondition:并行解析 classpath

class OnClassCondition extends FilteringSpringBootCondition {
  protected final ConditionOutcome[] getOutcomes(String[], AutoConfigurationMetadata);
  private ConditionOutcome[] resolveOutcomesThreaded(String[], AutoConfigurationMetadata);
  public ConditionOutcome getMatchOutcome(ConditionContext, AnnotatedTypeMetadata);
}

实现思路:

  • 单个判断(getMatchOutcome)读 @ConditionalOnClass / @ConditionalOnMissingClass 的 value 与 name,用 ClassLoader 尝试 Class.forName(resolve(...)),成功/失败决定匹配,并生成「required class X found」这类消息。
  • 批量判断(getOutcomes)走 resolveOutcomesThreaded,把候选类名分段交给多个线程并行解析。classpath 检查是纯 CPU + 类加载,可并行;启动时几百个自动配置类的类存在性检查并行化能省下可观时间。
  • 它只在 PARSE_CONFIGURATION 阶段工作——classpath 在启动前就固定了,不需要等 Bean 定义。

OnBeanCondition:唯一进入 REGISTER_BEAN 的条件

class OnBeanCondition extends FilteringSpringBootCondition
    implements org.springframework.context.annotation.ConfigurationCondition {
  public ConfigurationCondition$ConfigurationPhase getConfigurationPhase();   // -> REGISTER_BEAN
  protected final ConditionOutcome[] getOutcomes(String[], AutoConfigurationMetadata);
  public ConditionOutcome getMatchOutcome(ConditionContext, AnnotatedTypeMetadata);
  protected final OnBeanCondition$MatchResult getMatchingBeans(OnBeanCondition$Spec<?>);
}

它支持的条件注解最丰富:@ConditionalOnBean / @ConditionalOnMissingBean / @ConditionalOnSingleCandidate。判断逻辑都收敛到 getMatchingBeans(Spec):

  • Spec(实测存在,含 SingleCandidateSpec)描述「要找什么」:按类型、按名字、按注解、是否忽略某些类型、以及 search 策略。SearchStrategy 实测有三个值 CURRENT / ANCESTORS / ALL,分别表示只在当前上下文、只在祖先上下文、以及包含全部祖先上下文中查找。
  • 返回 MatchResult,再由 getMatchOutcome 转成 ConditionOutcome。
  • 类型匹配时会考虑 parameterizedContainer(如 List<Foo>、ObjectProvider<Foo>),所以 @ConditionalOnBean 能识别被包在容器类型里的 Bean。

为什么必须 REGISTER_BEAN:@ConditionalOnMissingBean 是自动配置「退让」的核心——它要等用户自己的 Bean 定义注册完,才敢确认「确实没有」。把它放在解析阶段会得到不确定的结果,因此 Spring Boot 专门让它晚一步求值。

代价是:OnBeanCondition 是三个过滤条件里最贵的(要遍历 Bean 定义、做类型解析),所以它在批量过滤阶段能做的检查有限,真正的判断多发生在 REGISTER_BEAN 阶段。

OnWebApplicationCondition:环境形状判断

class OnWebApplicationCondition extends FilteringSpringBootCondition {
  protected ConditionOutcome[] getOutcomes(String[], AutoConfigurationMetadata);
  public ConditionOutcome getMatchOutcome(ConditionContext, AnnotatedTypeMetadata);
}

@ConditionalOnWebApplication / @ConditionalOnNotWebApplication 判断当前应用是不是 Web 应用,并区分类型:

public interface ConditionalOnWebApplication {
  public abstract ConditionalOnWebApplication$Type type();   // ANY / SERVLET / REACTIVE
}

上一节那个真实例子就是它:WebMvcAutoConfiguration 标着 @ConditionalOnWebApplication(type = SERVLET) 且 @ConditionalOnClass({Servlet.class, DispatcherServlet.class})——只有「Servlet 型 Web 应用 + classpath 上有 Servlet API」时才装配。判断方式是通过 ConditionContext 探测容器里是否存在 Web 相关的特征类/Bean。

评估结果如何被缓存与记录

所有 SpringBootCondition 的 matches 都会把结果写入 ConditionEvaluationReport:

"$JAVA_HOME/bin/javap" -cp "$AC" org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport
public final class org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport {
  public void recordConditionEvaluation(String, Condition, ConditionOutcome);
  public void recordExclusions(java.util.Collection<java.lang.String>);
  public void recordEvaluationCandidates(java.util.List<java.lang.String>);
  public java.util.Map<String, ConditionAndOutcomes> getConditionAndOutcomesBySource();
  public java.util.List<java.lang.String> getExclusions();
  public java.util.Set<java.lang.String> getUnconditionalClasses();
  public ConditionEvaluationReport getParent();
  public static ConditionEvaluationReport find(BeanFactory);
  public static ConditionEvaluationReport get(ConfigurableListableBeanFactory);
  public ConditionEvaluationReport getDelta(ConditionEvaluationReport);
}

要点:

  • recordConditionEvaluation(source, condition, outcome) 以配置类为 key 累积一个 ConditionAndOutcomes,所以报告天然是「按来源类分组」的。getDelta(...) 支持父子上下文对比,用于「父上下文评估过什么、子上下文新增了什么」。
  • 记录者 ConditionEvaluationReportAutoConfigurationImportListener 注册在 spring.factories 的 AutoConfigurationImportListener key 下,负责把导入阶段(解析配置类之前)已经做出的批量过滤结论也并进报告,所以 --debug 能看到完整的两轮结果。
  • recordExclusions(...) 单独记下被 spring.autoconfigure.exclude 排除的类,报告里表现为 Exclusions: 段。

输出格式由 ConditionEvaluationReportLogger + ConditionEvaluationReportMessage 负责,实测固定四段:

Positive matches:
Negative matches:
Exclusions:
Unconditional classes:

每条记录形如 XxxAutoConfiguration matched: 或 Did not match:,下面缩进列出各个条件的 Matched: / Did not match: 与消息。ConditionEvaluationReportLogger 只接受 INFO 或 DEBUG 级别(否则报 'logLevel' must be INFO or DEBUG)。

@Conditional 结果本身不跨上下文复用:每个 ApplicationContext 有自己的报告,getParent() 只是关联查看。想「缓存」某次昂贵判断,应在自定义 Condition 里自己做,而不是假设框架会跨上下文缓存。

你能做什么

自定义条件。写一个 Condition 并配注解:

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class OnFeatureFlagCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment().getProperty("feature.new-billing", Boolean.class, false);
    }
}

想接入 --debug 报告与统一消息,就继承 SpringBootCondition 并实现 getMatchOutcome(...),返回 ConditionOutcome.match(...) 或 ConditionOutcome.noMatch(...),而不是直接实现 Condition。

组合条件。AnyNestedCondition / AllNestedConditions / NoneNestedConditions 三个基类(都在 o.s.boot.autoconfigure.condition 包)让你用静态内部类把既有条件拼成 or / and / not。

排障清单:

  • 条件「应该成立却没成立」:先看 --debug 的 Negative matches:,消息会写清缺哪个类 / 哪个 Bean / 属性值不符。
  • @ConditionalOnBean 结果不稳定:检查它所在配置类的解析顺序,必要时改用 REGISTER_BEAN 阶段(实现 ConfigurationCondition)或调整 @AutoConfigureAfter。
  • 属性条件误判:@ConditionalOnProperty 的 matchIfMissing 默认为 false,未配置时不匹配;havingValue 默认空串表示「存在且不为 false」。
  • 类存在性检查失败:@ConditionalOnClass(name = "...") 用字符串可避免编译依赖,但类名写错会静默判为不存在——对照 jar 核一遍。

小结

  • @Conditional 的求值发生在配置类解析阶段(ConditionEvaluator.shouldSkip),不是 Bean 创建阶段;命中即整段跳过,Bean 定义不会注册。
  • ConditionContext 只给五个入口(classloader / beanFactory / registry / environment / resourceLoader),映射到五类条件。
  • ConfigurationCondition 提供第二次机会:PARSE_CONFIGURATION(默认,OnClassCondition)与 REGISTER_BEAN(OnBeanCondition,因为要看 Bean 定义)。
  • SpringBootCondition 用 final matches 固定「记录评估结果」这一步,子类只实现 getMatchOutcome 返回 ConditionOutcome;FilteringSpringBootCondition 再叠加批量 getOutcomes,用于导入前的快速过滤。
  • OnClassCondition 并行做类存在性检查;OnBeanCondition 是唯一进 REGISTER_BEAN 的条件,也是最贵的;OnWebApplicationCondition 判断应用形状。
  • 所有结果汇总进 ConditionEvaluationReport,由 ConditionEvaluationReportLogger 输出为 Positive / Negative / Exclusions / Unconditional 四段。

条件决定了「装不装」,下一节回答「装了之后用户怎么让它退让、怎么把它排除」。

阅读导航:上一节:2.1 自动配置类的加载顺序 · 下一节:2.3 自动配置的覆盖与排除 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计