Spring Boot 4.1.1 深度源码解析-第 15 章 · 条件注解体系:全部 `@ConditionalOnXxx` 的实现原理

0 阅读50分钟

Spring Boot 4.1.1 深度源码解析-第 15 章 · 条件注解体系:全部 @ConditionalOnXxx 的实现原理

本章目标:把 org.springframework.boot.autoconfigure.condition 包下 47 个源文件、5,555 行代码逐层拆开, 回答三个问题——

  1. Boot 如何决定"一个自动配置类 / 一个 @Bean 方法该不该生效"?
  2. 为什么有些条件在类上写"是对的",在 @Bean 方法上写"是错的"?
  3. 为什么"条件求值顺序会影响结果"是 Boot 最著名的陷阱之一?

0. 先摆事实:条件注解在 Boot 4.1.1 里的真实用量

在动笔分析之前,先做一次实证统计——对 core/ 与 module/ 下所有非测试 Java 文件做全文匹配, 统计每个条件注解出现在多少个文件里:

条件注解出现文件数排名
@ConditionalOnClass3801
@ConditionalOnMissingBean3552
@ConditionalOnBean2093
@ConditionalOnBooleanProperty764
@ConditionalOnProperty595
@ConditionalOnWebApplication101—
@ConditionalOnSingleCandidate39
@ConditionalOnThreading11
@ConditionalOnMissingClass12
@ConditionalOnMissingFilterBean7
@ConditionalOnResource5
@ConditionalOnJndi4
@ConditionalOnCloudPlatform2
@ConditionalOnExpression1
@ConditionalOnWarDeployment1
@ConditionalOnNotWebApplication1
@ConditionalOnCheckpointRestore1
@ConditionalOnJava0

(@ConditionalOnWebApplication 的 101 超过了 76,但它是"三态"注解,实际语义密度不如 BooleanProperty, 所以仍把后者排第 4。这张表只是数量,不代表重要性。)

四条立刻可读出的结论:

  1. @ConditionalOnClass 是绝对主力(380 个文件)。Boot 的"按需装配"本质就是 classpath 探测—— 你加什么依赖,决定哪些自动配置活过来。这是"约定优于配置"最粗粒度也最可靠的一层。
  2. @ConditionalOnMissingBean 紧随其后(355)。它是"用户优先"的实现载体。
  3. @ConditionalOnBooleanProperty(76)已压过 @ConditionalOnProperty(59)。 前者是 3.5.0 新增(§7.5),Boot 自己的自动配置正在向它迁移——语义更明确。
  4. @ConditionalOnJava 用量为 0。这个 1.1.0 就存在的注解在 4.1.1 里一次都没用过: 基线已抬到 Java 17,更低的版本根本跑不起来 Boot,按 JVM 版本分流的需求消失了。

对照第 14 章:spring-boot-autoconfigure 自己的 *.imports 只有 12 行,全仓库却有 178 个 .imports。 绝大多数自动配置类都带条件——条件注解不是"可选装饰",而是自动配置的呼吸系统。


1. 两个接口:Condition 与 ConfigurationCondition

Boot 的条件全部建立在 Spring Framework 的 org.springframework.context.annotation.Condition 之上:

// Spring Framework(org.springframework.context.annotation)
@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

它有一个子接口,专门给"配置类条件"用:

public interface ConfigurationCondition extends Condition {
    ConfigurationPhase getConfigurationPhase();
    enum ConfigurationPhase {
        PARSE_CONFIGURATION,   // 解析配置类阶段:还没注册任何 Bean 定义
        REGISTER_BEAN          // 注册 Bean 阶段:本配置类的 Bean 定义即将注册
    }
}

1.1 为什么必须区分两个阶段

ConfigurationClassPostProcessor 在 refresh() 里跑,它处理 @Configuration 类时分两步:

ConfigurationClassParser.parse(配置类)
  ├─ 阶段 A:读注解元数据(ASM,不加载类)
  │     └─ 求值所有 PARSE_CONFIGURATION 阶段的条件
  │         → 不匹配:整个配置类被丢弃,连它的 @Bean 方法都不解析
  │
  └─ ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(配置类)
        ├─ 阶段 B:注册配置类自己的 BeanDefinition
        └─ 阶段 B:遍历每个 @Bean 方法
              └─ 求值所有 REGISTER_BEAN 阶段的条件(包括方法上的)
                  → 不匹配:这个 @Bean 定义不注册

PARSE_CONFIGURATION 阶段 BeanFactory 里几乎没有 Bean 定义;REGISTER_BEAN 阶段, 前面所有配置类的 Bean 定义都已注册完毕。所以——

凡是"看 BeanFactory 里有没有某个 Bean"的条件,都必须是 REGISTER_BEAN 阶段。 在 PARSE_CONFIGURATION 阶段问"有没有 DataSource",答案永远是"没有", 自动配置会傻乎乎地创建默认 DataSource,然后和用户自己定义的撞车。

这就是 OnBeanCondition 硬编码返回 REGISTER_BEAN 的原因:

// OnBeanCondition.java:88-94
@Order(Ordered.LOWEST_PRECEDENCE)
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition {

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }

而 OnClassCondition、OnPropertyCondition 等不实现 ConfigurationCondition—— 它们是"纯 Condition",在两个阶段都会被求值(Framework 的 ConditionEvaluator 对每个 Condition 都会调用;ConfigurationCondition 只是在特定阶段被跳过)。

1.2 一个容易写错的坑

如果你自己写一个条件,继承了 SpringBootCondition 但忘了实现 ConfigurationCondition, 那么它会在 PARSE_CONFIGURATION 阶段就被求值一次。如果它内部去查 BeanFactory,你会得到一个 早了整整一个阶段的答案。

// 错误示范
public class MyCondition extends SpringBootCondition {
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 危险:这个条件在 PARSE_CONFIGURATION 阶段也会跑
        String[] names = context.getBeanFactory().getBeanNamesForType(MyService.class);
        return new ConditionOutcome(names.length > 0, ConditionMessage.empty());
    }
}

// 正确示范
public class MyCondition extends SpringBootCondition implements ConfigurationCondition {
    @Override public ConfigurationPhase getConfigurationPhase() { return ConfigurationPhase.REGISTER_BEAN; }
    @Override public ConditionOutcome getMatchOutcome(...) { /* 同上 */ }
}

AbstractNestedCondition 直接把这件事固化进了 API——它的构造器强制你传一个 phase:

// AbstractNestedCondition.java:47-50
AbstractNestedCondition(ConfigurationPhase configurationPhase) {
    Assert.notNull(configurationPhase, "'configurationPhase' must not be null");
    this.configurationPhase = configurationPhase;
}

三个子类(AnyNestedCondition / AllNestedConditions / NoneNestedConditions)的构造器 都要求子类显式 super(phase),所以你写不出一个忘记声明阶段的嵌套条件。 这是"用 API 形状强制正确性"的范例。


2. SpringBootCondition:所有 Boot 条件的模板基类

// SpringBootCondition.java:39-62(节选)
public abstract class SpringBootCondition implements Condition {

    private final Log logger = LogFactory.getLog(getClass());

    @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) {
            throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
                    + ex.getMessage() + " not found. Make sure your own configuration does not rely on "
                    + "that class. This can also happen if you are "
                    + "@ComponentScanning a springframework package (e.g. if you "
                    + "put a @ComponentScan in the default package by mistake)", ex);
        }
        catch (RuntimeException ex) {
            throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
        }
    }

六条设计细节,每一条都是踩坑的痕迹:

2.1 matches 是 final

子类只能实现 getMatchOutcome。这是典型的模板方法模式——把"记日志 + 记报告 + 兜异常" 这三件横切事锁死在基类里,子类不可能忘记做。

推论:Boot 的条件永远会被写进 ConditionEvaluationReport。因为 recordEvaluation 在 final 方法里,子类绕不开。这也是为什么 /actuator/conditions 能给出那么完整的报告。

2.2 NoClassDefFoundError 被单独捕获,错误信息里带诊断建议

NoClassDefFoundError 是 Error 不是 Exception,catch (RuntimeException) 抓不到它。 Boot 专门为它写了一个 catch,并在消息里直接给出两种最可能的成因:

  1. "your own configuration does not rely on that class" —— 你的配置类引用了不存在的类
  2. "you are @ComponentScanning a springframework package (e.g. if you put a @ComponentScan in the default package by mistake)" —— 把启动类放在默认包(无 package 声明)

第 2 条是 Boot 用户最常见的自伤之一:主类没有 package 声明时,@ComponentScan 会扫 classpath 根, 把 org.springframework.* 全部扫进来。这条错误消息是无数 Stack Overflow 问题的答案。

2.3 getClassOrMethodName 与 getName 是两个方法,分隔符还不一样

private String getName(AnnotatedTypeMetadata metadata) {           // 用于异常消息
    if (metadata instanceof AnnotationMetadata annotationMetadata) {
        return annotationMetadata.getClassName();
    }
    if (metadata instanceof MethodMetadata methodMetadata) {
        return methodMetadata.getDeclaringClassName() + "." + methodMetadata.getMethodName();  // 点号
    }
    return metadata.toString();
}

private static String getClassOrMethodName(AnnotatedTypeMetadata metadata) {   // 用于日志和报告
    if (metadata instanceof ClassMetadata classMetadata) {
        return classMetadata.getClassName();
    }
    MethodMetadata methodMetadata = (MethodMetadata) metadata;
    return methodMetadata.getDeclaringClassName() + "#" + methodMetadata.getMethodName();      // 井号
}
  • 日志 / 报告里:com.example.Foo#bar(#,符合 Java 字节码的方法签名惯例)
  • 异常消息里:com.example.Foo.bar(.,更像 Java 源码)

这个不一致没有功能影响,但它是"同一件事被两处独立实现"的典型痕迹。 如果你在自定义条件里想打印方法名,注意别混用。

顺带注意 getClassOrMethodName 的第二分支是强制转型 (MethodMetadata) metadata—— 如果两个 instanceof 都不成立(比如自定义 AnnotatedTypeMetadata),会抛 ClassCastException。 而 getName 第三分支是 metadata.toString() 兜底。两个方法的健壮性不一致。

2.4 logOutcome 只在 TRACE 级别输出

protected final void logOutcome(String classOrMethodName, ConditionOutcome outcome) {
    if (this.logger.isTraceEnabled()) {
        this.logger.trace(getLogMessage(classOrMethodName, outcome));
    }
}

也就是说,正常情况下你一条条件日志都看不到。想排查"为什么我的自动配置没生效", 两条路:

  1. 开 logging.level.org.springframework.boot.autoconfigure.condition=TRACE
  2. 用 /actuator/conditions(它读的是 ConditionEvaluationReport,不是日志)

2.5 recordEvaluation 有 null 守卫

private void recordEvaluation(ConditionContext context, String classOrMethodName, ConditionOutcome outcome) {
    if (context.getBeanFactory() != null) {
        ConditionEvaluationReport.get(context.getBeanFactory())
            .recordConditionEvaluation(classOrMethodName, this, outcome);
    }
}

ConditionContext.getBeanFactory() 在某些早期场景(比如 PARSE_CONFIGURATION 阶段的某些调用) 可能为 null。此时静默跳过记录——不报错,只是报告里少一条。

2.6 两个组合工具方法:anyMatches 和 matches

protected final boolean anyMatches(ConditionContext context, AnnotatedTypeMetadata metadata,
        Condition... conditions) {
    for (Condition condition : conditions) {
        if (matches(context, metadata, condition)) {
            return true;
        }
    }
    return false;
}

protected final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata, Condition condition) {
    if (condition instanceof SpringBootCondition springBootCondition) {
        return springBootCondition.getMatchOutcome(context, metadata).isMatch();
    }
    return condition.matches(context, metadata);
}

注意 matches 的分支:如果条件是 SpringBootCondition,走 getMatchOutcome(不写报告); 否则走标准 Condition.matches(由那个条件自己决定是否记录)。

这是刻意的短路——组合场景里,你只想要一个布尔结果,不希望每个子条件都往报告里塞一条 (会让报告噪声爆炸)。

一个有意思的推论:anyMatches 有 return true 的短路,所以后面的条件根本不会被求值。 这意味着"条件求值顺序"不仅影响正确性,还影响你能在报告里看到什么—— 被短路掉的条件不会出现在报告里。这是排查"为什么某个条件的记录不见了"时的关键知识点。


3. FilteringSpringBootCondition:一个类的两种身份

// FilteringSpringBootCondition.java:39-68(节选)
abstract class FilteringSpringBootCondition extends SpringBootCondition
        implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {

    private BeanFactory beanFactory;
    private ClassLoader beanClassLoader;

    @Override
    public boolean[] match(@Nullable String[] autoConfigurationClasses,
            AutoConfigurationMetadata autoConfigurationMetadata) {
        ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory);
        @Nullable ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
        boolean[] match = new boolean[outcomes.length];
        for (int i = 0; i < outcomes.length; i++) {
            ConditionOutcome outcome = outcomes[i];
            match[i] = (outcome == null || outcome.isMatch());     // ← 关键
            if (!match[i] && outcome != null) {
                String autoConfigurationClass = autoConfigurationClasses[i];
                Assert.state(autoConfigurationClass != null, "'autoConfigurationClass' must not be null");
                logOutcome(autoConfigurationClass, outcome);
                if (report != null) {
                    report.recordConditionEvaluation(autoConfigurationClass, this, outcome);
                }
            }
        }
        return match;
    }

3.1 "预筛"(pre-filter)路径 vs "正式求值"路径

这个类同时是:

身份入口时机数据来源
AutoConfigurationImportFiltermatch(String[], AutoConfigurationMetadata)第 14 章讲的 AutoConfigurationImportSelector 九步流程的第 6 步编译期生成的 spring-autoconfigure-metadata.properties
Conditionmatches(context, metadata)(继承自 SpringBootCondition)ConfigurationClassParser 处理配置类时运行时读取的注解元数据

为什么需要两条路径?

  • 预筛阶段:候选可能有 200+ 个自动配置类。此时还没有 ConditionContext,也没有 Environment, 只有类名字符串数组和一份编译期静态元数据。
  • 正式求值阶段:候选已经被筛到几十个,此时有完整上下文,可以精确判定。

性能差距:预筛只读一个 Properties 文件(一次 IO,全 JVM 共享); 正式求值要走 ASM 读每个类的字节码。差一个数量级。

3.2 outcome == null 视为"匹配"

match[i] = (outcome == null || outcome.isMatch());

outcome == null 的语义是"这个 filter 对这个类没有意见"。 比如 OnClassCondition.getOutcomes 对没有 @ConditionalOnClass 的类返回 null。

这是"保守默认值":宁可放过(让后续阶段再判),不可错杀。 如果 null 被视为不匹配,那么任何 filter 返回 null 都会误杀配置类。

3.3 只在"筛掉"时记录报告

if (!match[i] && outcome != null) {
    ...
    report.recordConditionEvaluation(autoConfigurationClass, this, outcome);
}

匹配的情况不记录。所以报告里看到的"Negative matches"(不匹配)来自这里, 而"Positive matches"(匹配)来自正式求值路径的 SpringBootCondition.recordEvaluation。

这解释了 /actuator/conditions 的一个现象:被预筛掉的自动配置类,报告里只有一条 OnClassCondition 的否定记录,没有"它本来还有什么条件"。因为预筛阶段它就被打死了, 后续的 @ConditionalOnProperty、@ConditionalOnMissingBean 根本没机会求值。

3.4 ClassNameFilter:PRESENT / MISSING

protected enum ClassNameFilter {
    PRESENT {
        @Override public boolean matches(String className, @Nullable ClassLoader classLoader) {
            return isPresent(className, classLoader);
        }
    },
    MISSING {
        @Override public boolean matches(String className, @Nullable ClassLoader classLoader) {
            return !isPresent(className, classLoader);
        }
    };

    abstract boolean matches(String className, @Nullable ClassLoader classLoader);

    private static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
        if (classLoader == null) {
            classLoader = ClassUtils.getDefaultClassLoader();
        }
        try {
            resolve(className, classLoader);
            return true;
        }
        catch (Throwable ex) {          // ← 注意是 Throwable
            return false;
        }
    }
}

两个值得记住的点:

  1. catch (Throwable) —— 不只是 ClassNotFoundException。 NoClassDefFoundError、LinkageError、甚至 UnsupportedClassVersionError (用 JDK 21 编译的类被 JDK 17 加载)都会被当作"不存在"。 这是刻意的:classpath 探测的语义就是"能不能用",不只是"能不能找到"。
  2. classLoader == null 时回退到 ClassUtils.getDefaultClassLoader()。

3.5 resolve():一个"更快"的 Class.forName

/**
 * Slightly faster variant of {@link ClassUtils#forName(String, ClassLoader)} that
 * doesn't deal with primitives, arrays or inner types.
 */
protected static Class<?> resolve(String className, @Nullable ClassLoader classLoader)
        throws ClassNotFoundException {
    if (classLoader != null) {
        return Class.forName(className, false, classLoader);    // initialize = false
    }
    return Class.forName(className);                            // ← 这一支会初始化!
}

initialize = false 是关键:只加载不初始化,不跑 <clinit>。 classpath 探测不该有副作用。

但有一个不对称:classLoader == null 时走 Class.forName(className), 这个重载会执行静态初始化。理论上这是一个行为不一致。

实践上 classLoader 几乎不会是 null(BeanClassLoaderAware 会注入), 所以我判断这是一个理论瑕疵而非实际 bug。标注为按源码推断的观察, 未在 Boot 测试中找到专门覆盖 classLoader == null 分支的用例。


4. AutoConfigurationImportFilter:三个 filter 与它们的顺序

# core/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories:14-18
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

文件里的声明顺序是:OnBean → OnClass → OnWeb。

但每个类上都有 @Order:

filter@Order数值
OnClassCondition@Order(Ordered.HIGHEST_PRECEDENCE)Integer.MIN_VALUE
OnWebApplicationCondition@Order(Ordered.HIGHEST_PRECEDENCE + 20)MIN_VALUE + 20
OnBeanCondition@Order(Ordered.LOWEST_PRECEDENCE)Integer.MAX_VALUE

排序后的实际执行顺序是:OnClass → OnWeb → OnBean(与声明顺序相反)。

4.1 顺序是怎么来的?——一个推断链

AutoConfigurationImportSelector 的取值代码:

// AutoConfigurationImportSelector.java:278-293
protected List<AutoConfigurationImportFilter> getAutoConfigurationImportFilters() {
    return SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, this.beanClassLoader);
}

private ConfigurationClassFilter getConfigurationClassFilter() {
    ConfigurationClassFilter configurationClassFilter = this.configurationClassFilter;
    if (configurationClassFilter == null) {
        List<AutoConfigurationImportFilter> filters = getAutoConfigurationImportFilters();
        for (AutoConfigurationImportFilter filter : filters) {
            invokeAwareMethods(filter);
        }
        configurationClassFilter = new ConfigurationClassFilter(this.beanClassLoader, filters);
        this.configurationClassFilter = configurationClassFilter;
    }
    return configurationClassFilter;
}

Boot 自己没有排序。那么排序只能发生在 SpringFactoriesLoader.loadFactories() 里。

严谨声明:SpringFactoriesLoader 属于 Spring Framework 7.0.9,本仓库没有它的源码, 我无法逐行确认。但有一条反证支持这个结论: 如果 loadFactories 不排序,那么 OnClassCondition、OnWebApplicationCondition、 OnBeanCondition 上的 @Order 注解就是彻头彻尾的死代码—— Boot 里没有任何其他地方读取它们的顺序。 Boot 团队不会给三个类各写一个无用注解。 因此我判断 loadFactories 内部调用了 AnnotationAwareOrderComparator.sort()。 置信度:高。若要确证,需查看 spring-core 7.0.9 的 SpringFactoriesLoader.loadFactories。

同理,ConditionEvaluator(Framework)在求值类上的多个 @Conditional 时也会排序—— 这也是 OnClassCondition 要 HIGHEST_PRECEDENCE 的原因:最便宜的判定应该最先跑。

4.2 为什么这个顺序是"对的"

第 1 个跑:OnClassCondition          —— 只读 classpath,最便宜,也最常命中
第 2 个跑:OnWebApplicationCondition —— 只读 classpath(2 个类)+ 元数据
第 3 个跑:OnBeanCondition           —— 要扫 BeanFactory 的 BeanDefinition,最贵

便宜的判定先跑,把绝大多数候选先筛掉,贵的判定只对少数幸存者执行。 这是教科书式的"过滤器链排序"。

反过来说,如果 OnBeanCondition 先跑,它会遍历 getBeanNamesForType 对所有 200+ 个 自动配置类做类型查询——其中 90% 后面会被 OnClassCondition 一秒杀掉。纯浪费。

4.3 预筛的"保守性"代价

预筛读的是 AutoConfigurationMetadata(编译期由注解处理器生成的 META-INF/spring-autoconfigure-metadata.properties)。这个元数据只记录"这个类上有哪个条件注解", 不记录完整的判定逻辑。

所以预筛能做的事很有限:

// OnBeanCondition.getOutcomes(预筛路径)
Set<String> onBeanTypes = autoConfigurationMetadata.getSet(autoConfigurationClass, "ConditionalOnBean");
outcomes[i] = getOutcome(onBeanTypes, ConditionalOnBean.class);
if (outcomes[i] == null) {
    Set<String> onSingleCandidateTypes = autoConfigurationMetadata.getSet(autoConfigurationClass,
            "ConditionalOnSingleCandidate");
    outcomes[i] = getOutcome(onSingleCandidateTypes, ConditionalOnSingleCandidate.class);
}

注意这里的 getOutcome 只做一件事:检查 onBeanTypes 里的那些类在 classpath 上是否存在。

private @Nullable ConditionOutcome getOutcome(@Nullable Set<String> requiredBeanTypes,
        Class<? extends Annotation> annotation) {
    List<String> missing = filter(requiredBeanTypes, ClassNameFilter.MISSING, getBeanClassLoader());
    if (!missing.isEmpty()) {
        ConditionMessage message = ConditionMessage.forCondition(annotation)
            .didNotFind("required type", "required types").items(Style.QUOTE, missing);
        return ConditionOutcome.noMatch(message);
    }
    return null;
}

它的逻辑是:如果 @ConditionalOnBean(SomeType.class) 里的 SomeType 类根本不存在于 classpath, 那么"存在这个类型的 Bean"就绝无可能成立 → 直接判死。

这是纯粹的类型存在性检查,不是 Bean 检查。(名字叫 OnBeanCondition,预筛却完全不碰 Bean—— 因为此时 BeanFactory 里什么都没有。)


5. OnClassCondition:最快、也最需要小心的一个

@Order(Ordered.HIGHEST_PRECEDENCE)
class OnClassCondition extends FilteringSpringBootCondition {

5.1 双身份的分半并行

@Override
protected final @Nullable ConditionOutcome[] getOutcomes(@Nullable String[] autoConfigurationClasses,
        AutoConfigurationMetadata autoConfigurationMetadata) {
    // Split the work and perform half in a background thread if more than one
    // processor is available. Using a single additional thread seems to offer the
    // best performance. More threads make things worse.
    if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) {
        return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
    }
    else {
        OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0,
                autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader());
        return outcomesResolver.resolveOutcomes();
    }
}

四条硬事实:

  1. 只开一个额外线程。注释原话:"Using a single additional thread seems to offer the best performance. More threads make things worse." 这是实测结论——Boot 团队试过更多线程,更慢。 (Class.forName 大部分时间在等类加载器锁和 IO,多线程收益远小于线程创建 + 合并的开销。)
  2. 启用条件:length > 1 && availableProcessors() > 1。单核机器不并行。
  3. split = length / 2 —— 简单对半。
  4. 后台线程 catch (Throwable),join() 后在主线程 rethrowRuntimeException 重抛—— 异常栈与单线程执行一致,不因"在另一个线程"而丢失上下文。
private @Nullable ConditionOutcome[] resolveOutcomesThreaded(@Nullable 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());
    @Nullable ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes();   // ← 主线程同步算后半
    @Nullable ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes();     // ← join 前半
    @Nullable ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
    System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length);
    System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length);
    return outcomes;
}

顺序的巧思:createOutcomesResolver 构造器里就 thread.start(),后台线程立刻开始跑前半; 然后主线程同步算后半;最后 join() 等前半。两个半区真正并行。 若反过来(先算后半再启动线程),就退化成串行。

5.2 正式求值路径:getMatchOutcome

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    ClassLoader classLoader = context.getClassLoader();
    ConditionMessage matchMessage = ConditionMessage.empty();
    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));
    }
    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);
}

onClasses != null 的判据是"getAllAnnotationAttributes 返回非 null",即"这个类上有 @ConditionalOnClass"。不是"列表非空"——如果你写 @ConditionalOnClass({})(空数组), onClasses 是空 list 但非 null,会进入分支,missing 为空,然后 filter(PRESENT) 也是空 → 条件匹配。

所以 @ConditionalOnClass 不写任何类 = 恒真条件(一个 no-op)。

5.3 getCandidates:value 与 name 的合并

private @Nullable List<String> getCandidates(AnnotatedTypeMetadata metadata, Class<?> annotationType) {
    MultiValueMap<String, @Nullable Object> attributes = metadata
        .getAllAnnotationAttributes(annotationType.getName(), true);   // ← true = classValuesAsString
    if (attributes == null) { return null; }
    List<String> candidates = new ArrayList<>();
    addAll(candidates, attributes.get("value"));
    addAll(candidates, attributes.get("name"));
    return candidates;
}

第二个参数 true 是 classValuesAsString —— Class<?> 会被转成 String 类名。 这就是为什么后面 addAll 里能直接 (String[]) item 强转。

这两个属性是"且"关系(都在 candidates 里,全部必须 present)。

5.4 @ConditionalOnClass 在 @Bean 方法上的著名陷阱

@ConditionalOnClass 的 Javadoc 里有一段长警告,值得完整引用:

Note: Extra care must be taken when using @ConditionalOnClass on @Bean methods where typically the return type is the target of the condition. Before the condition on the method applies, the JVM will have loaded the class and potentially processed method references which will fail if the class is not present. To handle this scenario, a separate @Configuration class should be used to isolate the condition.

问题本质:@Bean 方法的返回类型就是 SomeService。JVM 加载 MyAutoConfiguration 并解析方法签名时就必须解析 SomeService——此时条件还没求值。类不在 classpath 就直接 NoClassDefFoundError,@ConditionalOnClass 根本没机会保护你。

Boot 官方给的写法(Javadoc 里的例子):

@AutoConfiguration
public class MyAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(SomeService.class)      // ← 放在嵌套配置类上
    public static class SomeServiceConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public SomeService someService() {
            return new SomeService();
        }
    }
}

原理:把条件提升到类级别。类条件在 PARSE_CONFIGURATION 阶段求值, 此时 JVM 还没加载 SomeService(ASM 读字节码不触发类加载)。条件不成立 → 嵌套类被跳过 → 它的 @Bean 方法从不被解析 → SomeService 永不被加载。

通用形式:

@ConditionalOnClass 要写在"类型边界"上(类、嵌套类),不能写在"类型内部"(@Bean 方法)。

同类风险:ConditionalOnMissingBean 的 value 是 Class<?>[] 时也有, 所以 Javadoc 反复强调"用作元注解时只能用 type(字符串)属性"。


6. OnBeanCondition:936 行的最大条件

这是整个 condition 包里最大的一个类。它一个人处理三个注解: @ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnSingleCandidate。

6.1 三条硬事实先摆出来

  1. @Order(Ordered.LOWEST_PRECEDENCE) —— 最贵,最后跑
  2. implements ConfigurationCondition,phase = REGISTER_BEAN
  3. 预筛路径只处理 @ConditionalOnBean 和 @ConditionalOnSingleCandidate, 完全不处理 @ConditionalOnMissingBean

第 3 条最值得解释。为什么?

因为"缺某个 Bean"在预筛阶段永远成立——此时 BeanFactory 里什么都没有。 如果预筛对 @ConditionalOnMissingBean 也做判定,结果必然是"全部匹配", 毫无筛选效果,纯粹浪费时间。 而且更糟:AutoConfigurationImportFilter 的语义是"如果不匹配就剔除"。 若 @ConditionalOnMissingBean 在预筛被误判为"不匹配",自动配置会被错误剔除。

这是一个"筛选器只能做保守剔除"的通用原则的完美例证。

6.2 getMatchOutcome:三个注解的求值顺序是硬编码的

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    ConditionOutcome matchOutcome = ConditionOutcome.match();
    MergedAnnotations annotations = metadata.getAnnotations();
    if (annotations.isPresent(ConditionalOnBean.class)) {
        Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
        matchOutcome = evaluateConditionalOnBean(spec, matchOutcome.getConditionMessage());
        if (!matchOutcome.isMatch()) {
            return matchOutcome;                       // ← 短路
        }
    }
    if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
        Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata,
                metadata.getAnnotations());
        matchOutcome = evaluateConditionalOnSingleCandidate(spec, matchOutcome.getConditionMessage());
        if (!matchOutcome.isMatch()) {
            return matchOutcome;                       // ← 短路
        }
    }
    if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
        Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
                ConditionalOnMissingBean.class);
        matchOutcome = evaluateConditionalOnMissingBean(spec, matchOutcome.getConditionMessage());
        if (!matchOutcome.isMatch()) {
            return matchOutcome;                       // ← 短路
        }
    }
    return matchOutcome;
}

顺序:@ConditionalOnBean → @ConditionalOnSingleCandidate → @ConditionalOnMissingBean, 每一步都短路。

这带来两个可观察后果:

  1. 消息累积:matchOutcome.getConditionMessage() 被传给下一个 spec, 所以最终消息是"OnBean 结果 and OnSingleCandidate 结果 and OnMissingBean 结果"的拼接。
  2. 短路 = 后面的注解不进报告。如果 @ConditionalOnBean 判否, 那么同一处的 @ConditionalOnMissingBean 不会出现在报告里—— 排查时要注意"你看不到 ≠ 它不存在"。

注意一个细节不一致:第一处用 annotations.isPresent(ConditionalOnBean.class)(类型安全), 后两处用 metadata.isAnnotated(Xxx.class.getName())(字符串)。 功能等价,但风格不统一——又是历史演进的痕迹。

6.3 Spec:把注解属性翻译成"搜索规格"

Spec(ConditionContext context, AnnotatedTypeMetadata metadata, MergedAnnotations annotations,
        Class<A> annotationType) {
    MultiValueMap<String, @Nullable Object> attributes = annotations.stream(annotationType)
        .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes))
        .collect(MergedAnnotationCollectors.toMultiValueMap(Adapt.CLASS_TO_STRING));
    MergedAnnotation<A> annotation = annotations.get(annotationType);
    this.context = context;
    this.annotationType = annotationType;
    this.names = extract(attributes, "name");
    this.annotations = extract(attributes, "annotation");
    this.ignoredTypes = resolveWhenPossible(extract(attributes, "ignored", "ignoredType"));
    this.parameterizedContainers = resolveWhenPossible(extract(attributes, "parameterizedContainer"));
    this.strategy = annotation.getValue("search", SearchStrategy.class).orElse(null);
    Set<BeanType> types = resolveWhenPossible(extractTypes(attributes));
    BeanTypeDeductionException deductionException = null;
    if (types.isEmpty() && this.names.isEmpty() && this.annotations.isEmpty()) {
        try {
            types = deducedBeanType(context, metadata);
        }
        catch (BeanTypeDeductionException ex) {
            deductionException = ex;
        }
    }
    this.types = types;
    validate(deductionException);
}

七步:

步动作备注
1annotations.stream(annotationType) + unique(getMetaTypes)支持元注解(composed annotation);unique 去重
2Adapt.CLASS_TO_STRINGClass<?> → 字符串类名(避免触发类加载)
3提取 name / annotation
4提取 ignored + ignoredType 并解析两个属性合并
5提取 parameterizedContainer
6annotation.getValue("search", SearchStrategy.class).orElse(null)null = 未指定
7若 type/name/annotation 全空 → 尝试推断 @Bean 方法返回类型

第 7 步就是" @ConditionalOnMissingBean 不写参数时,默认用 @Bean 方法的返回类型"的实现:

private Set<BeanType> deducedBeanType(ConditionContext context, AnnotatedTypeMetadata metadata) {
    if (metadata instanceof MethodMetadata methodMetadata && metadata.isAnnotated(Bean.class.getName())) {
        return deducedBeanTypeForBeanMethod(context, methodMetadata);
    }
    return Collections.emptySet();
}

private Set<BeanType> deducedBeanTypeForBeanMethod(ConditionContext context, MethodMetadata metadata) {
    try {
        return Set.of(getReturnType(context, metadata));
    }
    catch (Throwable ex) {
        throw new BeanTypeDeductionException(metadata.getDeclaringClassName(), metadata.getMethodName(), ex);
    }
}

两个必要条件:metadata 必须是 MethodMetadata(即标注在方法上), 且该方法必须有 @Bean。如果标注在类上且没写 type/name/annotation → 推断失败 → 抛异常。

protected void validate(@Nullable BeanTypeDeductionException ex) {
    if (!hasAtLeastOneElement(getTypes(), getNames(), getAnnotations())) {
        String message = getAnnotationName() + " did not specify a bean using type, name or annotation";
        if (ex == null) {
            throw new IllegalStateException(message);
        }
        throw new IllegalStateException(message + " and the attempt to deduce the bean's type failed", ex);
    }
}

这是"用失败代替静默"的又一例(第 12、13 章都见过这个范式): @ConditionalOnMissingBean 写在类上又不指定类型,直接启动失败, 而不是"静默地什么都不检查"(那样会导致自动配置永远生效,覆盖用户 Bean)。

6.4 类型推断:getReturnType 与泛型解包

private BeanType getReturnType(ConditionContext context, MethodMetadata metadata)
        throws ClassNotFoundException, LinkageError {
    // Safe to load at this point since we are in the REGISTER_BEAN phase
    ClassLoader classLoader = context.getClassLoader();
    ResolvableType returnType = getMethodReturnType(metadata, classLoader);
    if (isParameterizedContainer(returnType.resolve())) {
        returnType = returnType.getGeneric();
    }
    return new BeanType(returnType.toString(), returnType);
}

注释很关键:"Safe to load at this point since we are in the REGISTER_BEAN phase"。 在 REGISTER_BEAN 阶段加载类是安全的——因为此时 JVM 已经把所有需要的类都加载了。 这正是 §1.1 讲的"为什么必须区分 phase"。

isParameterizedContainer:如果返回类型本身是一个"容器"(比如 FilterRegistrationBean<MyFilter>), 则取它的泛型参数(MyFilter)。这就是 @ConditionalOnMissingFilterBean (= @ConditionalOnMissingBean(parameterizedContainer = FilterRegistrationBean.class))的工作原理。

findBeanMethod 的兜底:

private Method findBeanMethod(Class<?> declaringClass, String methodName) {
    Method method = ReflectionUtils.findMethod(declaringClass, methodName);
    if (isBeanMethod(method)) { return method; }
    Method[] candidates = ReflectionUtils.getAllDeclaredMethods(declaringClass);
    for (Method candidate : candidates) {
        if (candidate.getName().equals(methodName) && isBeanMethod(candidate)) { return candidate; }
    }
    throw new IllegalStateException("Unable to find bean method " + methodName);
}

为什么要兜底?因为重载。findMethod 只按名字找,可能找到一个没有 @Bean 的重载版本。 所以要遍历所有同名方法,找真正带 @Bean 的那个。

6.5 MatchResult:allMatched vs anyMatched 的语义鸿沟

这是整个 OnBeanCondition 最反直觉的设计点:

boolean isAllMatched() {
    return this.unmatchedAnnotations.isEmpty() && this.unmatchedNames.isEmpty()
            && this.unmatchedTypes.isEmpty();
}

boolean isAnyMatched() {
    return (!this.matchedAnnotations.isEmpty()) || (!this.matchedNames.isEmpty())
            || (!this.matchedTypes.isEmpty());
}
注解用的判据含义
@ConditionalOnBeanisAllMatched()所有 type / name / annotation 都要找到
@ConditionalOnSingleCandidateisAllMatched()同上
@ConditionalOnMissingBeanisAnyMatched()只要找到任何一个,就判否

对称性破缺:名字上 @ConditionalOnBean 和 @ConditionalOnMissingBean 像是"取反"关系, 但它们的判据不是 !isAllMatched(),而是 isAnyMatched()。

推演一下 @ConditionalOnMissingBean(type = {A.class, B.class}):

  • 找到 A,没找到 B → isAnyMatched() = true → 判否(不匹配,不创建 Bean)
  • 找到 A,找到 B → true → 判否
  • 都没找到 → isAnyMatched() = false → 判是(创建 Bean)

所以语义是"A 和 B 都得缺才生效"。这与 @ConditionalOnMissingBean 的 Javadoc 一致:

The condition matches when no bean of each class specified is contained in the BeanFactory.

注意 "no bean of each class"——是"每个类都没有",不是"不是每一个类都有"。

实践陷阱:

@Bean
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
public DataSource dataSource() { ... }

本意可能是"缺 DataSource 或 缺 XADataSource 时创建", 实际语义是"两个都缺才创建"。只要有一个存在,这个 Bean 就不会被创建。 正确写法是拆成两个 @Bean 方法,或用 AnyNestedCondition。

6.6 getMatchingBeans:搜索主流程

protected final MatchResult getMatchingBeans(Spec<?> spec) {
    ConfigurableListableBeanFactory beanFactory = getSearchBeanFactory(spec);
    ClassLoader classLoader = spec.getContext().getClassLoader();
    boolean considerHierarchy = spec.getStrategy() != SearchStrategy.CURRENT;
    Set<BeanType> parameterizedContainers = spec.getParameterizedContainers();
    MatchResult result = new MatchResult();
    Set<String> beansIgnoredByType = getNamesOfBeansIgnoredByType(beanFactory, considerHierarchy,
            spec.getIgnoredTypes(), parameterizedContainers);
    for (BeanType type : spec.getTypes()) {
        Map<String, @Nullable BeanDefinition> typeMatchedDefinitions = getBeanDefinitionsForType(beanFactory,
                considerHierarchy, type, parameterizedContainers);
        Set<String> typeMatchedNames = matchedNamesFrom(typeMatchedDefinitions,
                (name, definition) -> !ScopedProxyUtils.isScopedTarget(name)
                        && isCandidate(beanFactory, name, definition, beansIgnoredByType));
        if (typeMatchedNames.isEmpty()) { result.recordUnmatchedType(type); }
        else { result.recordMatchedType(type, typeMatchedNames); }
    }
    for (String annotation : spec.getAnnotations()) {
        // ... 类似,用 getBeanDefinitionsForAnnotation
    }
    for (String beanName : spec.getNames()) {
        if (!beansIgnoredByType.contains(beanName) && containsBean(beanFactory, beanName, considerHierarchy)) {
            result.recordMatchedName(beanName);
        }
        else { result.recordUnmatchedName(beanName); }
    }
    return result;
}

四个阶段:

  1. 先算出 ignored 集合(getNamesOfBeansIgnoredByType)—— 一次算好,后面每个 type 都用它过滤
  2. 遍历 types → getBeanNamesForType
  3. 遍历 annotations → getBeanNamesForAnnotation
  4. 遍历 names → containsBean / containsLocalBean

6.7 isCandidate:autowireCandidate + defaultCandidate

private boolean isCandidate(ConfigurableListableBeanFactory beanFactory, String name,
        @Nullable BeanDefinition definition, Set<String> ignoredBeans) {
    if (ignoredBeans.contains(name)) { return false; }
    if (definition == null || (definition.isAutowireCandidate() && isDefaultCandidate(definition))) {
        return true;
    }
    if (ScopedProxyUtils.isScopedTarget(name)) {
        try {
            BeanDefinition originalDefinition = beanFactory
                .getBeanDefinition(ScopedProxyUtils.getOriginalBeanName(name));
            if (originalDefinition.isAutowireCandidate() && isDefaultCandidate(originalDefinition)) {
                return true;
            }
        }
        catch (NoSuchBeanDefinitionException ex) {
        }
    }
    return false;
}

判定链:

1. 在 ignored 里 → false
2. definition == null(来自父上下文,拿不到定义)→ true
3. isAutowireCandidate() == true 且 isDefaultCandidate() == true → true
4. 是 scoped target("scopedTarget.xxx")→ 查原始定义,重复判定 3
5. 否则 → false

两个标志是什么?

  • autowireCandidate(BeanDefinition#isAutowireCandidate):能否被自动装配选中,@Bean(autowireCandidate = false) 可关。
  • defaultCandidate(AbstractBeanDefinition#isDefaultCandidate,Framework 6.2 新增):能否作为"按类型查找"的默认候选。

为什么检查它们? @ConditionalOnMissingBean 的语义是"用户没提供这个 Bean"。 一个 Bean 虽然存在但被标记为"不参与自动装配",就相当于"用户没提供它作为候选"——自动配置应补位。

scoped target:Spring 为 @SessionScope / @RequestScope 的 Bean 建代理时,会额外注册 scopedTarget.xxx 目标定义,其 autowireCandidate 可能是 false。 OnBeanCondition 特意回溯到原始定义再判一次。

6.8 SearchStrategy:三个值,三种"搜索域"

public enum SearchStrategy {
    /** Search only the current context. */
    CURRENT,
    /** Search all ancestors, but not the current context. */
    ANCESTORS,
    /** Search the entire hierarchy. */
    ALL
}

默认(未指定):Spec.getStrategy() 返回 SearchStrategy.ALL。

private SearchStrategy getStrategy() {
    return (this.strategy != null) ? this.strategy : SearchStrategy.ALL;
}

三种行为差异:

值considerHierarchy搜索的 BeanFactory
CURRENTfalse当前 BeanFactory,用 containsLocalBean
ANCESTORStrue父 BeanFactory(当前被完全排除)
ALLtrue当前 + 所有祖先(递归)

ANCESTORS 的实现很特别——它替换了 beanFactory:

private ConfigurableListableBeanFactory getSearchBeanFactory(Spec<?> spec) {
    ConfigurableListableBeanFactory beanFactory = spec.getContext().getBeanFactory();
    Assert.state(beanFactory != null, "'beanFactory' must not be null'");
    if (spec.getStrategy() == SearchStrategy.ANCESTORS) {
        BeanFactory parent = beanFactory.getParentBeanFactory();
        Assert.state(parent instanceof ConfigurableListableBeanFactory,
                "Unable to use SearchStrategy.ANCESTORS without ConfigurableListableBeanFactory");
        beanFactory = (ConfigurableListableBeanFactory) parent;
    }
    return beanFactory;
}

注意:只取直接父,不是所有祖先。而 considerHierarchy 又是 true(因为 ANCESTORS != CURRENT), 所以后续的 collectBeanDefinitionsForType 会从这个父开始继续向上递归。

净效果:ANCESTORS = "当前被跳过,从父开始往上全部"。 与枚举的 Javadoc "Search all ancestors, but not the current context" 一致。

仓库里的真实用法(这是最有说服力的部分):

// servlet/actuate/web/ServletManagementChildContextConfiguration.java:62
@ConditionalOnBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN, search = SearchStrategy.ANCESTORS)

Actuator 的管理端口子上下文里,要判断"主上下文里有没有 Spring Security 的过滤器链"。 必须用 ANCESTORS——因为子上下文的父就是主上下文。

// jetty/autoconfigure/servlet/JettyServletWebServerAutoConfiguration.java:68
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)

// tomcat/autoconfigure/servlet/TomcatServletWebServerAutoConfiguration.java:67
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)

这个更微妙:Tomcat 和 Jetty 的自动配置都检查"有没有 ServletWebServerFactory"。 如果用默认的 ALL,那么当父上下文里有一个 ServletWebServerFactory 时, 子上下文里的 Tomcat 自动配置就会不生效——这通常是错的 (子上下文想要自己的 Web 服务器)。所以用 CURRENT。

// integration/autoconfigure/IntegrationAutoConfiguration.java:178
@ConditionalOnMissingBean(value = IntegrationMBeanExporter.class, search = SearchStrategy.CURRENT)

同理。

规律:CURRENT 用于"这个上下文自己有没有", ANCESTORS 用于"主上下文有没有"(子上下文场景), ALL(默认)用于"整个层次里有没有"(绝大多数普通场景)。

6.9 getBeanNamesForType 的三个参数

result = putAll(result, beanFactory.getBeanNamesForType(type.resolvableType(), true, false), beanFactory);

ListableBeanFactory.getBeanNamesForType(Class, includeNonSingletons, allowEagerInit):

  • includeNonSingletons = true —— prototype 也算
  • allowEagerInit = false —— 不触发 FactoryBean 的提前初始化

allowEagerInit = false 很重要:如果为 true,为了确定 FactoryBean 产出的类型, Spring 会实例化这个 FactoryBean。在条件求值阶段做实例化是危险的 (可能触发副作用、循环依赖、甚至失败)。

代价:allowEagerInit = false 时,对于 FactoryBean 且类型信息不完整的情况, getBeanNamesForType 可能查不到。这是"安全性 vs 完整性"的取舍。

对照第 13 章:LazyInitializationBeanFactoryPostProcessor 里也用了 getBeansOfType(..., false, false)——同样的 allowEagerInit = false。同一个范式。

6.10 泛型容器展开:parameterizedContainer

for (BeanType parameterizedContainer : parameterizedContainers) {
    Class<?> resolved = parameterizedContainer.resolvableType().resolve();
    Assert.state(resolved != null, "'resolved' must not be null");
    ResolvableType generic = ResolvableType.forClassWithGenerics(resolved, type.resolvableType());
    result = putAll(result, beanFactory.getBeanNamesForType(generic, true, false), beanFactory);
}

即:除了查 Name,还要查 NameRegistration<Name>。

@ConditionalOnMissingFilterBean 就是这个机制的产物:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ConditionalOnMissingBean(parameterizedContainer = FilterRegistrationBean.class)
public @interface ConditionalOnMissingFilterBean {
    @AliasFor(annotation = ConditionalOnMissingBean.class)
    Class<? extends Filter>[] value() default {};
}

它等价于 @ConditionalOnMissingBean(value = XxxFilter.class, parameterizedContainer = FilterRegistrationBean.class) —— 既能识别直接注册的 Filter Bean,也能识别通过 FilterRegistrationBean<Filter> 注册的。

这是一个"用元注解封装复杂条件"的漂亮例子。

6.11 getBeanNamesForAnnotation:定义 + 单例双通道

private String[] getBeanNamesForAnnotation(ListableBeanFactory beanFactory,
        Class<? extends Annotation> annotationType) {
    Set<String> foundBeanNames = new LinkedHashSet<>();
    for (String beanName : beanFactory.getBeanDefinitionNames()) {          // 通道 1:BeanDefinition
        // ... 跳过 abstract 定义
        if (beanFactory.findAnnotationOnBean(beanName, annotationType, false) != null) {
            foundBeanNames.add(beanName);
        }
    }
    if (beanFactory instanceof SingletonBeanRegistry singletonBeanRegistry) {  // 通道 2:已注册单例
        for (String beanName : singletonBeanRegistry.getSingletonNames()) {
            if (beanFactory.findAnnotationOnBean(beanName, annotationType) != null) {
                foundBeanNames.add(beanName);
            }
        }
    }
    return foundBeanNames.toArray(String[]::new);
}

两个通道:

  1. BeanDefinition 通道:遍历所有 Bean 定义,跳过 abstract 的(抽象定义不能实例化)
  2. 单例通道:遍历已注册的单例实例,直接从实例的类上找注解

第 2 个通道是为了覆盖"手动 registerSingleton 注册的 Bean"—— 比如第 13 章讲的 springApplicationArguments、springBootBanner, 它们没有 BeanDefinition,只有单例实例。

注意两处 findAnnotationOnBean 的参数不同: 定义通道传了第三个参数 false(allowFactoryBeanInit = false),单例通道用两参版本。 这是同一个"不触发提前初始化"的考量。

6.12 SingleCandidateSpec:单候选的四级降序判定

先说 Spec 的特殊化:

private static class SingleCandidateSpec extends Spec<ConditionalOnSingleCandidate> {

    private static final Collection<String> FILTERED_TYPES = Arrays.asList("", Object.class.getName());

    @Override
    protected Set<String> extractTypes(@Nullable MultiValueMap<String, @Nullable Object> attributes) {
        Set<String> types = super.extractTypes(attributes);
        types.removeAll(FILTERED_TYPES);
        return types;
    }

    @Override
    protected void validate(@Nullable BeanTypeDeductionException ex) {
        Assert.isTrue(getTypes().size() == 1,
                () -> getAnnotationName() + " annotations must specify only one type (got "
                        + StringUtils.collectionToCommaDelimitedString(getTypes()) + ")");
    }
}

FILTERED_TYPES = ["", "java.lang.Object"] —— 为什么要剔除?

因为 @ConditionalOnSingleCandidate 的两个属性默认值:

Class<?> value() default Object.class;
String type() default "";

如果用户不写,extractTypes 会得到 {"java.lang.Object", ""}。 这两个是"未指定"的哨兵值,必须剔除,否则:

  • 会误以为用户要查 Object 类型(那会匹配所有 Bean)
  • validate 的 size() == 1 断言会失败(size 是 2)

validate 强制 size() == 1 —— 这是"用断言表达 API 契约"。 @ConditionalOnSingleCandidate 语义上只能指一个类型("单一候选"), 多个类型是没有意义的。

然后看判定逻辑(四级降序):

private ConditionOutcome evaluateConditionalOnSingleCandidate(Spec<ConditionalOnSingleCandidate> spec,
        ConditionMessage matchMessage) {
    MatchResult matchResult = getMatchingBeans(spec);
    if (!matchResult.isAllMatched()) {
        return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
    }
    Set<String> allBeans = matchResult.getNamesOfAllMatches();
    if (allBeans.size() == 1) {
        return ConditionOutcome.match(spec.message(matchMessage)
            .found("a single bean").items(Style.QUOTE, allBeans));
    }
    ConfigurableListableBeanFactory beanFactory = spec.context.getBeanFactory();
    Assert.state(beanFactory != null, "'beanFactory' must not be null");
    Map<String, @Nullable BeanDefinition> beanDefinitions = getBeanDefinitions(beanFactory, allBeans,
            spec.getStrategy() == SearchStrategy.ALL);
    List<String> primaryBeans = getPrimaryBeans(beanDefinitions);
    if (primaryBeans.size() == 1) {
        return ConditionOutcome.match(spec.message(matchMessage)
            .found("a single primary bean '" + primaryBeans.get(0) + "' from beans")
            .items(Style.QUOTE, allBeans));
    }
    if (primaryBeans.size() > 1) {
        return ConditionOutcome.noMatch(spec.message().found("multiple primary beans")
            .items(Style.QUOTE, primaryBeans));
    }
    List<String> nonFallbackBeans = getNonFallbackBeans(beanDefinitions);
    if (nonFallbackBeans.size() == 1) {
        return ConditionOutcome.match(spec.message(matchMessage)
            .found("a single non-fallback bean '" + nonFallbackBeans.get(0) + "' from beans")
            .items(Style.QUOTE, allBeans));
    }
    return ConditionOutcome.noMatch(spec.message().found("multiple beans").items(Style.QUOTE, allBeans));
}

四级:

级别 1:找到的 Bean 只有 1 个                         → 匹配
级别 2:有多个,但恰好 1 个是 @Primary                → 匹配
级别 2':有多个 @Primary                              → 不匹配(歧义)
级别 3:没有 @Primary,但恰好 1 个不是 fallback       → 匹配
级别 4:否则                                          → 不匹配

fallback 是什么? BeanDefinition#isFallback()(Spring Framework 6.2 新增)。 标记一个 Bean 为"兜底候选"——只有在没有其他候选时才用它。 @ConditionalOnSingleCandidate 的语义是"自动装配会成功", 而 fallback Bean 的存在不影响自动装配成功,所以要排除。

语义总结:@ConditionalOnSingleCandidate(X.class) = "注入 X 类型的依赖不会失败"。 这就是为什么它比 @ConditionalOnBean(X.class) 严格得多。

6.13 BeanType record 与 ResolvableType.NONE

record BeanType(String name, ResolvableType resolvableType) {
    @Override
    public String toString() {
        return ResolvableType.NONE.equals(this.resolvableType) ? this.name : this.resolvableType.toString();
    }
}

resolveWhenPossible 里:

for (String className : classNames) {
    try {
        Class<?> type = resolve(className, this.context.getClassLoader());
        resolved.add(new BeanType(className, ResolvableType.forRawClass(type)));
    }
    catch (ClassNotFoundException | NoClassDefFoundError ex) {
        resolved.add(new BeanType(className, ResolvableType.NONE));
    }
}

类找不到时不抛异常,而是塞一个 ResolvableType.NONE。

然后 collectBeanDefinitionsForType 里:

if (ResolvableType.NONE.equals(type.resolvableType())) {
    return result;
}

直接跳过——即"要求一个 classpath 上不存在的类型的 Bean" → 视为"没找到"。

为什么不抛? 因为 @ConditionalOnBean(type = "com.example.MaybeAbsent") (用字符串属性)本来就是"类可能不存在"的用法。 @ConditionalOnMissingBean(type = "...") 同理—— "缺这个类型的 Bean"在类不存在时天然成立。

BeanType.toString() 的分支:NONE 时返回原始字符串(因为 ResolvableType.NONE.toString() 会显示成 none),否则返回 ResolvableType 的字符串(含泛型信息,如 com.example.Foo<java.lang.String>)。 这是为了让条件报告可读。


7. OnPropertyCondition:一个 Condition 管六个注解

@Order(Ordered.HIGHEST_PRECEDENCE + 40)
class OnPropertyCondition extends SpringBootCondition {

它处理:

注解引入版本作用
@ConditionalOnProperty1.1.0属性存在且值符合
@ConditionalOnProperties3.5.0上面注解的容器(@Repeatable)
@ConditionalOnBooleanProperty3.5.0布尔语义版本
@ConditionalOnBooleanProperties3.5.0上面的容器

7.1 六个注解怎么被统一处理

private Stream<MergedAnnotation<Annotation>> stream(MergedAnnotations mergedAnnotations) {
    return Stream.concat(stream(mergedAnnotations, ConditionalOnProperty.class, ConditionalOnProperties.class),
            stream(mergedAnnotations, ConditionalOnBooleanProperty.class, ConditionalOnBooleanProperties.class));
}

private Stream<MergedAnnotation<Annotation>> stream(MergedAnnotations mergedAnnotations,
        Class<? extends Annotation> type, Class<? extends Annotation> containerType) {
    return Stream.concat(stream(mergedAnnotations, type), streamRepeated(mergedAnnotations, type, containerType));
}

四个流拼接:直接注解 + 容器里的重复注解,两个注解类型各一份。

然后:

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    MergedAnnotations mergedAnnotations = metadata.getAnnotations();
    List<MergedAnnotation<Annotation>> annotations = stream(mergedAnnotations).toList();
    List<ConditionMessage> noMatch = new ArrayList<>();
    List<ConditionMessage> match = new ArrayList<>();
    for (MergedAnnotation<Annotation> annotation : annotations) {
        ConditionOutcome outcome = determineOutcome(annotation, context.getEnvironment());
        (outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
    }
    if (!noMatch.isEmpty()) {
        return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
    }
    return ConditionOutcome.match(ConditionMessage.of(match));
}

关键语义:只要有一个不匹配,整体就不匹配(AND)。 且不短路——所有注解都会被求值,消息全部收集。

这与 OnBeanCondition 的短路风格相反。为什么? 因为属性检查很便宜,而且报告里需要完整信息("哪一个属性不满足")。

7.2 Spec:属性的解析

Spec(Class<? extends Annotation> annotationType, AnnotationAttributes annotationAttributes) {
    this.annotationType = annotationType;
    this.prefix = (!annotationAttributes.containsKey("prefix")) ? "" : getPrefix(annotationAttributes);
    this.names = getNames(annotationAttributes);
    this.havingValue = getHavingValue(annotationAttributes);
    this.matchIfMissing = annotationAttributes.getBoolean("matchIfMissing");
}

getNames 的三条断言:

Assert.state(value.length > 0 || name.length > 0,
        () -> "The name or value attribute of @%s must be specified");
Assert.state(value.length == 0 || name.length == 0,
        () -> "The name and value attributes of @%s are exclusive");
  1. 必须指定 name 或 value(至少一个非空)
  2. name 和 value 互斥(不能同时非空)

value 和 name 是 @AliasFor 关系,语义相同,但同时写会被拒绝。 这是"用断言拒绝歧义"。

getPrefix:

private String getPrefix(AnnotationAttributes annotationAttributes) {
    String prefix = annotationAttributes.getString("prefix").trim();
    if (StringUtils.hasText(prefix) && !prefix.endsWith(".")) {
        prefix = prefix + ".";
    }
    return prefix;
}

自动补点:prefix = "spring.example" → "spring.example."。 所以 @ConditionalOnProperty(prefix = "spring", name = "example.values") → 查 spring.example.values。

trim() —— 前缀会被 trim,但 name 不会。

7.3 collectProperties:没有宽松绑定

private void collectProperties(PropertyResolver resolver, List<String> missing, List<String> nonMatching) {
    for (String name : this.names) {
        String key = this.prefix + name;
        if (resolver.containsProperty(key)) {
            if (!isMatch(resolver.getProperty(key), this.havingValue)) {
                nonMatching.add(name);
            }
        }
        else {
            if (!this.matchIfMissing) {
                missing.add(name);
            }
        }
    }
}

resolver 就是 context.getEnvironment()——它是 Environment,即 PropertyResolver。 containsProperty(key) 和 getProperty(key) 做的是精确键查找。

这是本章最重要的实践陷阱之一: @ConditionalOnProperty 不做宽松绑定。 @ConditionalOnProperty("myLongProperty") 匹配不到 my-long-property。 Javadoc 明确要求:"Use the dashed notation to specify each property"。

对比第 08 章讲的 Binder:Binder 会把 my-long-property、myLongProperty、MY_LONG_PROPERTY 全部归一化。@ConditionalOnProperty 走的是另一条路——它用的是 Environment.getProperty 这条古老通道,没有任何归一化。

7.4 isMatch:匹配表

private boolean isMatch(@Nullable String value, String requiredValue) {
    if (StringUtils.hasLength(requiredValue)) {
        return requiredValue.equalsIgnoreCase(value);
    }
    return !"false".equalsIgnoreCase(value);
}

把 Javadoc 那张表复现一遍(havingValue="" 即默认):

属性值havingValue=""havingValue="true"havingValue="false"havingValue="foo"
"true"✅✅❌❌
"false"❌❌✅❌
"foo"✅❌❌✅
缺失❌(除非 matchIfMissing=true)

三条读法:

  1. havingValue="" 时,语义是"值不等于 false 就匹配"。 所以 foo 也匹配——这不是 bug,"非空即真"是 Boot 的一贯约定。 (对照第 09 章:@ConfigurationProperties 的布尔绑定也遵循类似宽松规则。)
  2. equalsIgnoreCase —— 大小写不敏感。
  3. 属性缺失时的判定完全由 matchIfMissing 决定,与 havingValue 无关。

7.5 @ConditionalOnBooleanProperty:3.5.0 的新语义

boolean havingValue() default true;      // ← 注意是 boolean,默认 true
boolean matchIfMissing() default false;

与 @ConditionalOnProperty 的关键差异:

@ConditionalOnProperty@ConditionalOnBooleanProperty
havingValue 类型String,默认 ""boolean,默认 true
默认语义"值不等于 false""值等于 true"
引入1.1.03.5.0

这个差异非常大:

@ConditionalOnProperty("feature.enabled")          // feature.enabled=foo 也算开启!
@ConditionalOnBooleanProperty("feature.enabled")   // 只有 =true 才算开启

Boot 自己的自动配置正在向 @ConditionalOnBooleanProperty 迁移 (本章开头统计:76 vs 59,新注解已经反超),正是因为它把"布尔开关"的语义钉死了。

实践建议:新代码一律用 @ConditionalOnBooleanProperty, 只有在需要 havingValue="some-string" 这类非布尔判断时才用 @ConditionalOnProperty。

7.6 集合属性不可靠 —— 以及官方给的解法

@ConditionalOnProperty 的 Javadoc 有一段明确的警告:

This condition cannot be reliably used for matching collection properties. For example, in the following configuration, the condition matches if spring.example.values is present in the Environment but does not match if spring.example.values[0] is present.

@ConditionalOnProperty(prefix = "spring", name = "example.values")
class ExampleAutoConfiguration { }

原因:Environment.containsProperty 查的是原始 PropertySource 的键。 而 spring.example.values[0]=a 在 PropertySource 里的键就是 spring.example.values[0], 不是 spring.example.values。所以精确查找失败。

Boot 给的解法是 OnPropertyListCondition:

public abstract class OnPropertyListCondition extends SpringBootCondition {

    private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        BindResult<?> property = Binder.get(context.getEnvironment()).bind(this.propertyName, STRING_LIST);
        ConditionMessage.Builder messageBuilder = this.messageBuilder.get();
        if (property.isBound()) {
            return ConditionOutcome.match(messageBuilder.found("property").items(this.propertyName));
        }
        return ConditionOutcome.noMatch(messageBuilder.didNotFind("property").items(this.propertyName));
    }
}

它用的是第 08 章讲的 Binder!Bindable.listOf(String.class) 会做聚合绑定, 所以 spring.example.values[0]、spring.example.values=a,b,c 都能被识别。

这是"用 Binder 而不是 Environment"来解决宽松/聚合问题的典型例子, 也再次印证第 08 章的结论:Binder 是 Boot 属性处理的"正确入口"。

OnPropertyListCondition 是 abstract(@since 2.0.5), 需要子类传入 propertyName 和一个 messageBuilder 的 Supplier。 Supplier(而不是直接传 Builder)是为了每次求值都拿到新实例—— ConditionMessage.Builder 是有状态的。


8. OnWebApplicationCondition:一个 Condition 服务两个相反的注解

@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnWebApplicationCondition extends FilteringSpringBootCondition {

    private static final String SERVLET_WEB_APPLICATION_CLASS =
        "org.springframework.web.context.support.GenericWebApplicationContext";
    private static final String REACTIVE_WEB_APPLICATION_CLASS =
        "org.springframework.web.reactive.HandlerResult";

8.1 正式求值:靠 required 翻转语义

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    boolean required = metadata.isAnnotated(ConditionalOnWebApplication.class.getName());
    ConditionOutcome outcome = isWebApplication(context, metadata, required);
    if (required && !outcome.isMatch()) {
        return ConditionOutcome.noMatch(outcome.getConditionMessage());
    }
    if (!required && outcome.isMatch()) {
        return ConditionOutcome.noMatch(outcome.getConditionMessage());
    }
    return ConditionOutcome.match(outcome.getConditionMessage());
}

一个 Condition 服务两个注解:

  • @ConditionalOnWebApplication → required = true → 要求是 Web 应用
  • @ConditionalOnNotWebApplication → required = false → 要求不是 Web 应用

写法是"先算出 isWebApplication 的结果,再根据 required 决定怎么用"。 这比写两个 Condition 更简洁,且保证两个注解的判定逻辑绝对一致(不会出现 "两个注解对'什么是 Web 应用'有分歧")。

这是一个值得学习的模式:相反语义的注解,共享同一个判据,只翻转用法。

8.2 Servlet 判定的四级降序

private ConditionOutcome isServletWebApplication(ConditionContext context) {
    ConditionMessage.Builder message = ConditionMessage.forCondition("");
    if (ClassNameFilter.MISSING.matches(SERVLET_WEB_APPLICATION_CLASS, context.getClassLoader())) {
        return ConditionOutcome.noMatch(message.didNotFind("servlet web application classes").atAll());
    }
    if (context.getBeanFactory() != null) {
        String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
        if (ObjectUtils.containsElement(scopes, "session")) {
            return ConditionOutcome.match(message.foundExactly("'session' scope"));
        }
    }
    if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
        return ConditionOutcome.match(message.foundExactly("ConfigurableWebEnvironment"));
    }
    if (context.getResourceLoader() instanceof WebApplicationContext) {
        return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
    }
    return ConditionOutcome.noMatch(message.because("not a servlet web application"));
}

四级:

1. GenericWebApplicationContext 类存在?
   └─ 否 → 不匹配(连 servlet 栈都不在 classpath)
2. BeanFactory 注册了 "session" scope?
   └─ 是 → 匹配(Web 应用一定会注册 session scope)
3. Environment 是 ConfigurableWebEnvironment?
   └─ 是 → 匹配
4. ResourceLoader 是 WebApplicationContext?
   └─ 是 → 匹配
5. 都不满足 → 不匹配

第 1 级是"必要条件"(类都不在,肯定不是); 2–4 级是三条独立的"充分条件"(任一成立即可)。

注意第 2 级的判据很巧妙:session scope 只有 Web 应用才会注册。 这比检查 Environment 类型更早可用(在某些早期阶段 Environment 还没切换)。

8.3 Reactive 判定:只有三级

private ConditionOutcome isReactiveWebApplication(ConditionContext context) {
    ConditionMessage.Builder message = ConditionMessage.forCondition("");
    if (ClassNameFilter.MISSING.matches(REACTIVE_WEB_APPLICATION_CLASS, context.getClassLoader())) {
        return ConditionOutcome.noMatch(message.didNotFind("reactive web application classes").atAll());
    }
    if (context.getEnvironment() instanceof ConfigurableReactiveWebEnvironment) {
        return ConditionOutcome.match(message.foundExactly("ConfigurableReactiveWebEnvironment"));
    }
    if (context.getResourceLoader() instanceof ReactiveWebApplicationContext) {
        return ConditionOutcome.match(message.foundExactly("ReactiveWebApplicationContext"));
    }
    return ConditionOutcome.noMatch(message.because("not a reactive web application"));
}

没有 session scope 这一级——因为响应式栈不注册 session scope。

8.4 Type.ANY 的合并逻辑

private ConditionOutcome isAnyWebApplication(ConditionContext context, boolean required) {
    ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnWebApplication.class,
            required ? "(required)" : "");
    ConditionOutcome servletOutcome = isServletWebApplication(context);
    if (servletOutcome.isMatch() && required) {
        return new ConditionOutcome(servletOutcome.isMatch(), message.because(servletOutcome.getMessage()));
    }
    ConditionOutcome reactiveOutcome = isReactiveWebApplication(context);
    if (reactiveOutcome.isMatch() && required) {
        return new ConditionOutcome(reactiveOutcome.isMatch(), message.because(reactiveOutcome.getMessage()));
    }
    return new ConditionOutcome(servletOutcome.isMatch() || reactiveOutcome.isMatch(),
            message.because(servletOutcome.getMessage()).append("and").append(reactiveOutcome.getMessage()));
}

required == false 时(即 @ConditionalOnNotWebApplication): 两个结果都算完,最后 ||。这是对的——"不是 Web 应用"要求两个都不是。

required == true 时:Servlet 匹配就立刻返回(短路),否则看 Reactive。

8.5 预筛路径

@Override
protected @Nullable ConditionOutcome[] getOutcomes(@Nullable String[] autoConfigurationClasses,
        AutoConfigurationMetadata autoConfigurationMetadata) {
    @Nullable ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
    for (int i = 0; i < outcomes.length; i++) {
        String autoConfigurationClass = autoConfigurationClasses[i];
        if (autoConfigurationClass != null) {
            outcomes[i] = getOutcome(
                    autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnWebApplication"));
        }
    }
    return outcomes;
}

private @Nullable ConditionOutcome getOutcome(@Nullable String type) {
    if (type == null) { return null; }
    ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnWebApplication.class);
    ClassNameFilter missingClassFilter = ClassNameFilter.MISSING;
    if (ConditionalOnWebApplication.Type.SERVLET.name().equals(type)) {
        if (missingClassFilter.matches(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
            return ConditionOutcome.noMatch(message.didNotFind("servlet web application classes").atAll());
        }
    }
    if (ConditionalOnWebApplication.Type.REACTIVE.name().equals(type)) {
        if (missingClassFilter.matches(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
            return ConditionOutcome.noMatch(message.didNotFind("reactive web application classes").atAll());
        }
    }
    if (missingClassFilter.matches(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())
            && !ClassUtils.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
        return ConditionOutcome.noMatch(message.didNotFind("reactive or servlet web application classes").atAll());
    }
    return null;
}

预筛只做 classpath 检查(因为此时没有 Environment / ResourceLoader)。

第三段的逻辑:

servlet 类缺失 && reactive 类也不存在 → 肯定不是 Web 应用 → 判死

注意这里用了 missingClassFilter.matches(...)(NOT present)和 !ClassUtils.isPresent(...) 两种写法——语义相同,风格不一致。

Type.ANY 的情形:type 字符串是 "ANY",前两个 if 都不进,只走第三段。正确。


9. 其余条件逐个拆解

9.1 OnJavaCondition:一个没人用的条件

@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnJavaCondition extends SpringBootCondition {

    private static final JavaVersion JVM_VERSION = JavaVersion.getJavaVersion();

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, @Nullable Object> attributes = metadata.getAnnotationAttributes(ConditionalOnJava.class.getName());
        Assert.state(attributes != null, "'attributes' must not be null");
        Range range = (Range) attributes.get("range");
        Assert.state(range != null, "'range' must not be null");
        JavaVersion version = (JavaVersion) attributes.get("value");
        Assert.state(version != null, "'version' must not be null");
        return getMatchOutcome(range, JVM_VERSION, version);
    }

JVM_VERSION 是 static final —— JVM 版本在类加载时就确定,之后永不改变。 所以这个条件的判定是常量时间的。

isWithin 只支持两个 range,其余抛 IllegalStateException:

private boolean isWithin(JavaVersion runningVersion, Range range, JavaVersion version) {
    if (range == Range.EQUAL_OR_NEWER) { return runningVersion.isEqualOrNewerThan(version); }
    if (range == Range.OLDER_THAN) { return runningVersion.isOlderThan(version); }
    throw new IllegalStateException("Unknown range " + range);
}

为什么 Boot 4.1.1 一次都不用? 因为基线已经抬到 Java 17, spring-boot 的字节码本身就是 17(Native 要求 25)。 "在 Java 17 以下禁用某某自动配置"这种需求已经不存在——低于 17 根本跑不起来 Boot。

9.2 OnThreadingCondition 与 Threading 枚举

class OnThreadingCondition extends SpringBootCondition {     // ← 注意:没有 @Order

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, @Nullable Object> attributes = metadata
            .getAnnotationAttributes(ConditionalOnThreading.class.getName());
        Assert.state(attributes != null, "'attributes' must not be null");
        Threading threading = (Threading) attributes.get("value");
        Assert.state(threading != null, "'threading' must not be null");
        return getMatchOutcome(context.getEnvironment(), threading);
    }

Threading 枚举的关键设计:

public enum Threading {
    PLATFORM {
        @Override public boolean isActive(Environment environment) {
            return !VIRTUAL.isActive(environment);      // ← PLATFORM = NOT VIRTUAL
        }
    },
    VIRTUAL {
        @Override public boolean isActive(Environment environment) {
            return environment.getProperty("spring.threads.virtual.enabled", boolean.class, false)
                    && JavaVersion.getJavaVersion().isEqualOrNewerThan(JavaVersion.TWENTY_ONE);
        }
    };
    public abstract boolean isActive(Environment environment);
}

两个条件都要满足才算 VIRTUAL:

  1. spring.threads.virtual.enabled=true
  2. Java 21+

即使你开了开关,在 Java 17 上也不会启用虚拟线程——静默降级为 PLATFORM。

PLATFORM 是 VIRTUAL 的否定 —— 这保证了"二者必居其一", 不会出现"两个都不 active"的漏洞。这是一个封闭性设计。

(注意:OnThreadingCondition 没有 @Order,所以它在多个条件排序时处于"无顺序"位置。 由于它是纯属性读取,很便宜,这个缺失影响不大。)

9.3 OnCloudPlatformCondition

class OnCloudPlatformCondition extends SpringBootCondition {
    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, @Nullable Object> attributes = metadata
            .getAnnotationAttributes(ConditionalOnCloudPlatform.class.getName());
        Assert.state(attributes != null, "'attributes' must not be null");
        CloudPlatform cloudPlatform = (CloudPlatform) attributes.get("value");
        Assert.state(cloudPlatform != null, "'cloudPlatform' must not be null");
        return getMatchOutcome(context.getEnvironment(), cloudPlatform);
    }

    private ConditionOutcome getMatchOutcome(Environment environment, CloudPlatform cloudPlatform) {
        String name = cloudPlatform.name();
        ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnCloudPlatform.class);
        if (cloudPlatform.isActive(environment)) {
            return ConditionOutcome.match(message.foundExactly(name));
        }
        return ConditionOutcome.noMatch(message.didNotFind(name).atAll());
    }
}

CloudPlatform.isActive(Environment) 的实现见第 10 章(8 个枚举值、强制 vs 探测的双路径)。 这里只强调:条件层完全委托给枚举,自己不做任何判定。 这是"把平台知识收敛到枚举"的良好分层。

仓库里只有 2 个文件用 @ConditionalOnCloudPlatform—— Boot 4.x 里云平台的差异化配置已经大量改用 spring.config.activate.on-cloud-platform(第 10 章), 条件注解退居次要。

9.4 ResourceCondition(抽象模板)与 OnResourceCondition

public abstract class ResourceCondition extends SpringBootCondition {

    private final String name;
    private final String property;
    private final String[] resourceLocations;

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (context.getEnvironment().containsProperty(this.property)) {
            return ConditionOutcome.match(startConditionMessage().foundExactly("property " + this.property));
        }
        return getResourceOutcome(context, metadata);      // 逐个 exists() 检查默认位置
    }

语义:

如果配置了 property(比如 spring.sql.init.schema-locations)→ 直接匹配,不管资源存不存在
否则 → 检查默认位置列表,任一存在即匹配

注意"属性存在即匹配"这一条——它不校验属性值指向的资源是否真的存在。 这是刻意的:属性是你显式指定的,找不到是配置错误,应该在使用时报错, 而不是让条件静默判否(那样会导致"我配了但没生效"的疑难杂症)。

这是"显式配置优先于自动探测"原则的体现。

OnResourceCondition(@Order(HIGHEST+20))是具体的资源条件:

public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    MultiValueMap<String, @Nullable Object> attributes = metadata
        .getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
    Assert.state(attributes != null, "'attributes' must not be null");
    ResourceLoader loader = context.getResourceLoader();
    List<String> locations = new ArrayList<>();
    List<@Nullable Object> resources = attributes.get("resources");
    Assert.state(resources != null, "'resources' must not be null");
    collectValues(locations, resources);
    Assert.state(!locations.isEmpty(),
            "@ConditionalOnResource annotations must specify at least one resource location");
    List<String> missing = new ArrayList<>();
    for (String location : locations) {
        String resource = context.getEnvironment().resolvePlaceholders(location);
        if (!loader.getResource(resource).exists()) { missing.add(location); }
    }
    if (!missing.isEmpty()) {
        return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnResource.class)
            .didNotFind("resource", "resources").items(Style.QUOTE, missing));
    }
    return ConditionOutcome.match(ConditionMessage.forCondition(ConditionalOnResource.class)
        .found("location", "locations").items(locations));
}

两个细节:

  1. resolvePlaceholders(location) —— 资源路径支持 ${}
  2. 空的 resources 会抛 IllegalStateException —— 又是"用失败代替静默"

9.5 OnExpressionCondition:SpEL 条件

@Order(Ordered.LOWEST_PRECEDENCE - 20)
class OnExpressionCondition extends SpringBootCondition {
    // ...
    expression = wrapIfNecessary(expression);                               // 自动补 #{...}
    expression = context.getEnvironment().resolvePlaceholders(expression);   // 后解析 ${}
    ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    if (beanFactory != null) {
        boolean result = evaluateExpression(beanFactory, expression);
        return new ConditionOutcome(result, messageBuilder.resultedIn(result));
    }
    return ConditionOutcome.noMatch(messageBuilder.because("no BeanFactory available."));
    // evaluateExpression: (result != null && (boolean) result)   ← 强制转型
}

五个要点:

  1. 自动包裹 #{} —— 可写 @ConditionalOnExpression("${my.prop} == 1") 而不必写 #{...}
  2. resolvePlaceholders 在包裹之后 —— 先变 #{...} 再解析 ${}
  3. @Order(LOWEST - 20) —— 最后跑(SpEL 求值最贵)
  4. 没有 BeanFactory 时判否(不是判是)
  5. (boolean) result 是强制转型 —— 表达式返回非布尔会 ClassCastException, 被 matches 的 catch (RuntimeException) 包成 IllegalStateException

@ConditionalOnExpression 在仓库里只有 1 处使用。Boot 官方不鼓励用它—— SpEL 无法被配置元数据工具分析,也无法被 AOT 静态化。 新代码应该用 @ConditionalOnProperty 或嵌套条件替代。

9.6 OnJndiCondition

@Order(Ordered.LOWEST_PRECEDENCE - 20)
class OnJndiCondition extends SpringBootCondition {

    @Override
    public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
            .fromMap(metadata.getAnnotationAttributes(ConditionalOnJndi.class.getName()));
        Assert.state(attributes != null, "'attributes' must not be null");
        String[] locations = attributes.getStringArray("value");
        try {
            return getMatchOutcome(locations);
        }
        catch (NoClassDefFoundError ex) {
            return ConditionOutcome
                .noMatch(ConditionMessage.forCondition(ConditionalOnJndi.class).because("JNDI class not found"));
        }
    }

catch (NoClassDefFoundError) —— JNDI 在 Java 11+ 已经从 JDK 移除(JEP 320), 所以 javax.naming 可能不存在。此时静默判否。

locations.length == 0 时的语义:只检查"JNDI 环境是否可用"(InitialContext 存在), 不查具体位置。

JndiLocator.lookupFirstLocation:逐个尝试,第一个成功的即返回(OR 语义):

public @Nullable String lookupFirstLocation() {
    for (String location : this.locations) {
        try { lookup(location); return location; }
        catch (NamingException ex) { /* Swallow and continue */ }
    }
    return null;
}

9.7 OnWarDeploymentCondition

boolean required = metadata.isAnnotated(ConditionalOnWarDeployment.class.getName());
ResourceLoader resourceLoader = context.getResourceLoader();
if (resourceLoader instanceof WebApplicationContext applicationContext) {
    ServletContext servletContext = applicationContext.getServletContext();
    if (servletContext != null) {
        return new ConditionOutcome(required, "Application is deployed as a WAR file.");
    }
}
return new ConditionOutcome(!required, ...because("the application is not deployed as a WAR file."));

与 OnWebApplicationCondition 同一个模式(required 翻转),但判定只有一条:ServletContext 是否非空。 ServletContext 非空意味着应用是被外部 Servlet 容器加载的(传统 WAR 部署), 而不是 Boot 自己启动内嵌服务器(内嵌场景下 ServletContext 后创建)。 仓库里 @ConditionalOnWarDeployment / @ConditionalOnNotWarDeployment 各用 1 次。

9.8 两个"其实是别的东西"的注解

@ConditionalOnCheckpointRestore:

@ConditionalOnClass(name = "org.crac.Resource")
public @interface ConditionalOnCheckpointRestore { }

它没有自己的 Condition 类——它就是一个 @ConditionalOnClass 的马甲。 CRaC(Coordinated Restore at Checkpoint)的 org.crac.Resource 类存在 = CRaC 可用。 (第 12 章讲过 CRaC 的 Startup.create() 双条件探测,这里用的是更简单的类探测。)

@ConditionalOnMissingFilterBean:

@ConditionalOnMissingBean(parameterizedContainer = FilterRegistrationBean.class)
public @interface ConditionalOnMissingFilterBean {
    @AliasFor(annotation = ConditionalOnMissingBean.class)
    Class<? extends Filter>[] value() default {};
}

它也是马甲——一个带 @AliasFor 的元注解(§6.10 已讲)。

方法论意义:Boot 里"看起来是独立条件"的注解,有相当一部分是组合注解。 判断一个条件注解的真实实现,唯一可靠的方法是看它的 @Conditional 指向哪个类。


10. 嵌套条件:AnyNestedCondition / AllNestedConditions / NoneNestedConditions

单个条件注解不够用时,Boot 提供了用嵌套类组合条件的机制。 这是 Boot 里唯一能表达"或"的官方手段(@ConditionalOnXxx 之间都是"且")。

10.1 一个真实例子

// module/spring-boot-jdbc/.../DataSourceAutoConfiguration.java:93
/**
 * {@link AnyNestedCondition} that checks that either {@code spring.datasource.type}
 * is set or {@link PooledDataSourceAvailableCondition} applies.
 */
static class PooledDataSourceCondition extends AnyNestedCondition {

    PooledDataSourceCondition() {
        super(ConfigurationPhase.PARSE_CONFIGURATION);
    }

    @ConditionalOnProperty("spring.datasource.type")
    static class ExplicitType { }

    @Conditional(PooledDataSourceAvailableCondition.class)
    static class PooledDataSourceAvailable { }

}

读法:"显式指定了 spring.datasource.type" 或者 "classpath 上有支持的连接池" —— 两者满足其一即可。

再看一个 NoneNestedConditions 的例子,逻辑更绕:

// module/spring-boot-grpc-client/.../CompositeChannelFactoryAutoConfiguration.java:55
static class MultipleNonPrimaryChannelFactoriesCondition extends NoneNestedConditions {

    MultipleNonPrimaryChannelFactoriesCondition() {
        super(ConfigurationPhase.REGISTER_BEAN);
    }

    @ConditionalOnMissingBean(GrpcChannelFactory.class)
    static class NoChannelFactoryCondition { }

    @ConditionalOnSingleCandidate(GrpcChannelFactory.class)
    static class SingleInjectableChannelFactoryCondition { }

}

读法:"既不是没有 ChannelFactory,也不是只有一个候选" —— 即"有多个 GrpcChannelFactory 且没有 唯一 primary"。此时才需要创建一个 CompositeGrpcChannelFactory 来聚合它们(所以类上有 @Primary)。

这个条件的价值在于,它把"存在多个 Bean"这个 Boot 原生条件无法直接表达的语义, 用"双重否定"构造出来了。这是一个值得记住的技巧。

仓库实证:全仓库有 20+ 处嵌套条件的使用,AnyNestedCondition 占绝大多数 (JCacheCacheConfiguration、CouchbaseAutoConfiguration、FlywayAutoConfiguration、 LiquibaseAutoConfiguration、CodecsAutoConfiguration、GsonHttpMessageConvertersConfiguration、 JndiConnectionFactoryAutoConfiguration 等),AllNestedConditions 与 NoneNestedConditions 各数处。

10.2 AbstractNestedCondition 的执行机制

@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
    String className = getClass().getName();
    MemberConditions memberConditions = new MemberConditions(context, this.configurationPhase, className);
    MemberMatchOutcomes memberOutcomes = new MemberMatchOutcomes(memberConditions);
    return getFinalMatchOutcome(memberOutcomes);
}

四步:

  1. 取自己的类名
  2. MemberConditions 扫描自己的成员类(getMemberClassNames())
  3. 对每个成员类,收集它上面的所有 @Conditional
  4. 逐个求值,交给子类的 getFinalMatchOutcome 汇总

关键点:成员类是空的(static class ExplicitType { }),它们唯一的用途是承载注解。 这是"用类型当命名空间"的技巧——因为 Java 注解不能嵌套,但类可以。

MemberConditions(ConditionContext context, ConfigurationPhase phase, String className) {
    this.context = context;
    this.readerFactory = new SimpleMetadataReaderFactory(context.getResourceLoader());
    String[] members = getMetadata(className).getMemberClassNames();
    this.memberConditions = getMemberConditions(members, phase, className);
}

注意 SimpleMetadataReaderFactory 是 new 出来的,不是共享的 CachingMetadataReaderFactory。 对照第 14 章:AutoConfigurationImportSelector.getMetadataReaderFactory() 会优先拿共享的 internalCachingMetadataReaderFactory。嵌套条件没走那条路—— 所以每次求值都会重新创建 factory。

由于嵌套条件只在每个自动配置类上跑一次(不是每个候选跑一次),这个开销可以接受。 标注为按代码观察的推断:我没有在 Boot 找到对此的性能注释或优化记录。

10.3 phase 校验:一个单向检查

private void validateMemberCondition(Condition condition, ConfigurationPhase nestedPhase,
        String nestedClassName) {
    if (nestedPhase == ConfigurationPhase.PARSE_CONFIGURATION
            && condition instanceof ConfigurationCondition configurationCondition) {
        ConfigurationPhase memberPhase = configurationCondition.getConfigurationPhase();
        if (memberPhase == ConfigurationPhase.REGISTER_BEAN) {
            throw new IllegalStateException("Nested condition " + nestedClassName + " uses a configuration "
                    + "phase that is inappropriate for " + condition.getClass());
        }
    }
}

只检查一种组合:外层是 PARSE_CONFIGURATION,成员是 REGISTER_BEAN → 抛异常。

为什么这一条必须禁止? 因为 PARSE_CONFIGURATION 阶段跑条件时,Bean 定义还没注册。 如果成员的 @ConditionalOnMissingBean 在此刻求值,得到的答案是"全部缺失"——结果必然错误。 所以 Boot 用异常硬拦。

反向为什么不禁止? 外层 REGISTER_BEAN + 成员 PARSE_CONFIGURATION: 此时 Bean 定义已注册,PARSE_CONFIGURATION 的条件(如 @ConditionalOnProperty)也能正确求值。 这是安全的,所以不拦。

这是"只禁止会出错的组合,允许无害的组合"的精确判断,不是一刀切。 与第 10 章 InvalidConfigDataPropertyException 的思路一脉相承。

10.4 getUltimateOutcome:单条件透传 vs 多条件全匹配

ConditionOutcome getUltimateOutcome() {
    ConditionMessage.Builder message = ConditionMessage
        .forCondition("NestedCondition on " + ClassUtils.getShortName(this.metadata.getClassName()));
    if (this.outcomes.size() == 1) {
        ConditionOutcome outcome = this.outcomes.get(0);
        return new ConditionOutcome(outcome.isMatch(), message.because(outcome.getMessage()));
    }
    List<ConditionOutcome> match = new ArrayList<>();
    List<ConditionOutcome> nonMatch = new ArrayList<>();
    for (ConditionOutcome outcome : this.outcomes) {
        (outcome.isMatch() ? match : nonMatch).add(outcome);
    }
    if (nonMatch.isEmpty()) {
        return ConditionOutcome.match(message.found("matching nested conditions").items(match));
    }
    return ConditionOutcome.noMatch(message.found("non-matching nested conditions").items(nonMatch));
}

一个成员类上如果有多个 @Conditional,它们是"且"关系(nonMatch.isEmpty())。

所以组合规则是:

成员类之间:由子类决定(Any = 或 / All = 且 / None = 全否)
成员类内部的多个 @Conditional:永远是"且"

10.5 三个子类的汇总逻辑

子类getFinalMatchOutcome 的 match@Order
AnyNestedCondition!getMatches().isEmpty()@Order(LOWEST_PRECEDENCE - 20)
AllNestedConditionsmatches.size() == all.size()无
NoneNestedConditionsgetMatches().isEmpty()无

只有 AnyNestedCondition 有 @Order,另外两个没有。 (又一次风格不统一的历史痕迹。)

三者的消息格式相同:先一条汇总("N matched M did not"),再逐条列出所有成员的结果。


11. ConditionMessage:一套报告文案 DSL

ConditionMessage(449 行)是 Boot 为条件报告设计的不可变文本构建器。 它本身不参与判定,但它决定了你能在 /actuator/conditions 里读到什么质量的信息。

核心 API 形状:

ConditionMessage.forCondition(ConditionalOnClass.class)      // "@ConditionalOnClass"
    .didNotFind("required class", "required classes")        // "did not find required class"
    .items(Style.QUOTE, missing);                            // "'a', 'b'"(自动单复数)

ConditionMessage.forCondition(ConditionalOnMissingBean.class, spec)   // 第二个参数是"细节",会走 spec.toString()
    .found("bean", "beans").items(Style.QUOTE, names);

message.because(reason);      // 追加 "because ..."
message.andCondition(X.class) // 追加另一个条件段
message.atAll();              // 追加 "at all"
message.foundExactly("x");    // 追加 "found x"
message.resultedIn(result);   // 追加 "resulted in true/false"

两个设计要点:

  1. 单复数成对传参(didNotFind("required class", "required classes"))—— Boot 宁可让调用方多写一个字符串,也不愿生成语法错误的报告。
  2. Style.QUOTE 让类名/属性名在报告里带引号,便于人眼扫描。

ConditionMessage.empty() 返回空消息,ConditionMessage.of(List<ConditionMessage>) 把多条消息拼成一条。


12. ConditionOutcome:判定的返回值

public class ConditionOutcome {
    private final boolean match;
    private final ConditionMessage message;

    public @Nullable String getMessage() {
        return this.message.isEmpty() ? null : this.message.toString();   // ← 空时返回 null
    }

    @Override public boolean equals(Object obj) { /* match + message */ }
    @Override public int hashCode() { return Boolean.hashCode(this.match) * 31 + ...; }
    @Override public String toString() { return (this.message != null) ? this.message.toString() : ""; }
}

三个 API 细节:

  1. getMessage() 在消息为空时返回 null(不是 ""),而 getConditionMessage() 返回对象。 两个 getter 语义不同——用错会得到 null。
  2. 实现了 equals / hashCode —— 这是为了 ConditionEvaluationReport.getDelta() 能比较两次运行的结果差异(Actuator 的 ConditionsReportEndpoint 在某些场景下用)。
  3. toString() 里的 this.message != null 判据恒为 true(message 是 final 且构造器 Assert.notNull)。这是一处冗余的防御性判断。

两个静态工厂:ConditionOutcome.match(...) / ConditionOutcome.noMatch(...) (各有 String 和 ConditionMessage 两个重载)。Javadoc 明确建议用 ConditionMessage 版本 "for more consistent messages"。


13. ConditionEvaluationReport:条件报告的数据结构

public final class ConditionEvaluationReport {
    private static final String BEAN_NAME = "autoConfigurationReport";
    private final SortedMap<String, ConditionAndOutcomes> outcomes = new TreeMap<>();
    private boolean addedAncestorOutcomes;
    private @Nullable ConditionEvaluationReport parent;
    private final List<String> exclusions = new ArrayList<>();
    private final Set<String> unconditionalClasses = new HashSet<>();

13.1 它是怎么存的

public static ConditionEvaluationReport get(ConfigurableListableBeanFactory beanFactory) {
    synchronized (beanFactory) {
        ConditionEvaluationReport report;
        if (beanFactory.containsSingleton(BEAN_NAME)) {
            report = beanFactory.getBean(BEAN_NAME, ConditionEvaluationReport.class);
        }
        else {
            report = new ConditionEvaluationReport();
            beanFactory.registerSingleton(BEAN_NAME, report);
        }
        locateParent(beanFactory.getParentBeanFactory(), report);
        return report;
    }
}

它是 registerSingleton 注册的单例,不是 Bean 定义—— 对照第 13 章讲的 springApplicationArguments / springBootBanner,同一手法: "不该被用户看到的内部对象"都用 registerSingleton。

synchronized (beanFactory) —— 以 BeanFactory 为锁,保证并发下只有一个 report。

TreeMap —— 报告按 source(类名或 类#方法)字典序排序,便于阅读。

13.2 unconditionalClasses:一个巧妙的反查机制

public void recordEvaluationCandidates(List<String> evaluationCandidates) {
    this.unconditionalClasses.addAll(evaluationCandidates);
}

public void recordConditionEvaluation(String source, Condition condition, ConditionOutcome outcome) {
    ...
    this.unconditionalClasses.remove(source);     // ← 一旦有记录,就不是"无条件"了
    ...
}

public Set<String> getUnconditionalClasses() {
    Set<String> filtered = new HashSet<>(this.unconditionalClasses);
    this.exclusions.forEach(filtered::remove);
    return Collections.unmodifiableSet(filtered);
}

用法:AutoConfigurationImportSelector 先把所有候选塞进 unconditionalClasses, 然后每有一条条件记录就把对应类移除。最后剩下的 = 从未被任何条件判定的类 = "无条件生效的自动配置"。

这在 /actuator/conditions 的 "Unconditional classes" 段落里显示。 这是一个"用排除法反向统计"的轻量化实现——不需要给每个类打标记。

13.3 addNoMatchOutcomeToAncestors:嵌套类的传播

private void addNoMatchOutcomeToAncestors(String source) {
    String prefix = source + "$";
    this.outcomes.forEach((candidateSource, sourceOutcomes) -> {
        if (candidateSource.startsWith(prefix)) {
            ConditionOutcome outcome = ConditionOutcome
                .noMatch(ConditionMessage.forCondition("Ancestor " + source).because("did not match"));
            sourceOutcomes.add(ANCESTOR_CONDITION, outcome);
        }
    });
}

语义:如果外层类 Foo 的条件不匹配,那么 Foo$Bar 这个嵌套配置类也不该生效。 报告里会给 Foo$Bar 补一条 Ancestor Foo did not match 的否定记录。

判据是字符串前缀 source + "$" —— 依赖 Java 嵌套类的命名约定 (字节码里 Foo$Bar)。这是一个"用命名约定代替对象引用"的做法—— 因为报告里只有字符串,拿不到 Class 对象。

addedAncestorOutcomes 标志保证这个传播只做一次(懒执行)。

13.4 getDelta:增量报告

public ConditionEvaluationReport getDelta(ConditionEvaluationReport previousReport) {
    ConditionEvaluationReport delta = new ConditionEvaluationReport();
    this.outcomes.forEach((source, sourceOutcomes) -> {
        ConditionAndOutcomes previous = previousReport.outcomes.get(source);
        if (previous == null || previous.isFullMatch() != sourceOutcomes.isFullMatch()) {
            sourceOutcomes.forEach((conditionAndOutcome) -> delta.recordConditionEvaluation(source,
                    conditionAndOutcome.getCondition(), conditionAndOutcome.getOutcome()));
        }
    });
    // exclusions / unconditionalClasses 同理做差集
    return delta;
}

只包含"新增的"或"匹配状态变了"的条目。 这个能力在需要对比两次启动差异时有用(比如 AOT 处理前后)。

13.5 AncestorsMatchedCondition:一个永不执行的 Condition

private static final class AncestorsMatchedCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        throw new UnsupportedOperationException();
    }
}

它只作为报告里的占位标记——addNoMatchOutcomeToAncestors 用它当 key。 matches() 直接抛异常,因为它永远不会被真的执行(它不是通过 @Conditional 注册的)。

这是"用一个假实现占位"的模式:报告需要一个 Condition 对象来标识"这条记录来自祖先失败", 但又不能真的去求值它。抛 UnsupportedOperationException 是最诚实的表达—— 比返回一个假布尔值好。


14. 全局求值顺序:一张完整的图

把本章 + 第 14 章串起来,一个自动配置类的条件求值顺序是这样的:

【第 0 层】候选加载
  ImportCandidates 读 178 个 *.imports(顺序不可依赖)
      ↓
【第 1 层】spring.autoconfigure.exclude 排除(第 14 章)
      ↓
【第 2 层】AutoConfigurationImportSelector 排序(第 14 章)
  @AutoConfigureBefore/After > @AutoConfigureOrder > 字母序
      ↓
【第 3 层】预筛(AutoConfigurationImportFilter,本章 §4)
  排序:OnClassCondition → OnWebApplicationCondition → OnBeanCondition
  数据源:编译期 spring-autoconfigure-metadata.properties
  语义:null = 通过;只在判否时记录报告
      ↓
【第 4 层】ConfigurationClassParser 解析配置类(PARSE_CONFIGURATION)
  排序:按 @Order(Framework 的 ConditionEvaluator 排序)
  HIGHEST      → OnClassCondition
  HIGHEST+20   → OnWebApplicationCondition / OnJavaCondition / OnResourceCondition
  HIGHEST+40   → OnPropertyCondition
  LOWEST-20    → OnExpressionCondition / OnJndiCondition / AnyNestedCondition
  LOWEST       → OnBeanCondition(但它不在此阶段跑,见下)
  数据源:运行时注解元数据(ASM)
  语义:不匹配 → 整个配置类被丢弃,@Bean 方法不解析
      ↓
【第 5 层】注册 Bean 定义(REGISTER_BEAN)
  OnBeanCondition(@ConditionalOnBean → OnSingleCandidate → OnMissingBean,短路)
  AnyNestedCondition / AllNestedConditions / NoneNestedConditions(若声明 REGISTER_BEAN)
  语义:不匹配 → 该 @Bean 定义不注册

三条从这张图里读出的结论:

  1. @ConditionalOnMissingBean 只在第 5 层生效。它依赖"前面的 Bean 定义都已注册", 而这正是第 14 章讲的 DeferredImportSelector 保证的—— 自动配置的导入被推迟到所有用户配置类处理完之后。 没有 DeferredImportSelector,@ConditionalOnMissingBean 就失效。
  2. @ConditionalOnClass 在第 3 层和第 4 层各跑一次。第 3 层是"粗筛"(只读元数据), 第 4 层是"精判"(读注解)。两次判定逻辑相同,但第 3 层看到的类更少(只看类级注解的类名字符串)。
  3. 同一处多个条件的"且"关系由 Framework 保证,但求值顺序由 @Order 决定, 而顺序会影响报告内容(短路导致后面的条件不出现)和性能。

15. 二十条反直觉结论

#结论
1@ConditionalOnClass 写在 @Bean 方法上通常是无效的——JVM 解析返回类型时类已被加载。要写在嵌套配置类上
2@ConditionalOnMissingBean({A, B}) 是"A 和 B 都缺才生效",不是"缺 A 或 缺 B"
3类上写 @ConditionalOnMissingBean 不指定 type/name/annotation → 启动失败(IllegalStateException),不是静默通过
4@ConditionalOnProperty 不做宽松绑定,myLongProperty 匹配不到 my-long-property
5havingValue="" 时语义是"值不等于 false",所以 foo 也算匹配
6@ConditionalOnBooleanProperty 默认语义是"值等于 true",与 @ConditionalOnProperty 不同
7OnBeanCondition 的预筛只处理 @ConditionalOnBean / @ConditionalOnSingleCandidate,不处理 @ConditionalOnMissingBean
8OnBeanCondition 三个注解的求值顺序是硬编码 + 短路的,被短路的注解不进报告
9ClassNameFilter 用 catch (Throwable),所以 UnsupportedClassVersionError 也算"类不存在"
10OnClassCondition 只开一个后台线程,注释说 "More threads make things worse"
11SearchStrategy 默认 ALL,但在子上下文场景必须显式用 CURRENT 或 ANCESTORS
12search = ANCESTORS 是"从父开始往上",当前上下文被完全跳过
13@ConditionalOnSingleCandidate 有四级降序判定:1 个 → 1 个 primary → 1 个非 fallback → 否
14@ConditionalOnSingleCandidate 的 value / type 默认值(Object.class / "")会被显式剔除
15嵌套条件里,PARSE_CONFIGURATION 外层 + REGISTER_BEAN 成员 → 抛异常;反向允许
16嵌套条件中,一个成员类上的多个 @Conditional 永远是"且";成员类之间才由 Any/All/None 决定
17ResourceCondition 在"属性存在"时直接匹配,不校验资源是否真的存在
18@ConditionalOnCheckpointRestore 其实是 @ConditionalOnClass(name = "org.crac.Resource")
19@ConditionalOnMissingFilterBean 其实是 @ConditionalOnMissingBean(parameterizedContainer = FilterRegistrationBean.class)
20预筛阶段被打死的自动配置类,报告里只有一条否定记录,它的其他条件从没被求值过

16. 动手验证清单

16.1 验证短路导致"条件不出现在报告里"

@Configuration
static class T {
    @Bean
    @ConditionalOnBean(DataSource.class)          // 必然不匹配(没有 DataSource)
    @ConditionalOnMissingBean(String.class)       // 也不会被求值
    public Object x() { return new Object(); }
}

打开 /actuator/conditions,在 "Negative matches" 里找 T#x: 只应看到 @ConditionalOnBean 的记录,看不到 @ConditionalOnMissingBean。

16.2 验证"两个都缺"语义

@Bean
@ConditionalOnMissingBean({ Object.class, String.class })
public Runnable probe() { return () -> {}; }

@Bean public Object marker() { return new Object(); }   // 让 Object 存在

期望:probe 不存在(Object 已存在 → isAnyMatched() = true → 判否)。

16.3 验证宽松绑定缺失

@ConditionalOnProperty("myLongProperty")   // application.yaml 里写 my-long-property: true

期望:自动配置不生效(containsProperty("myLongProperty") 返回 false)。 改成 @ConditionalOnProperty("my-long-property") 再试,生效。

16.4 验证 havingValue="" 的"非空即真"

my.feature=foo + @ConditionalOnProperty("my.feature") → 生效。 my.feature=false → 不生效。

16.5 验证 Threading 的版本门槛

Java 21+ 上设 spring.threads.virtual.enabled=true → @ConditionalOnThreading(VIRTUAL) 生效。 Java 17 上设同样的属性 → 仍然不生效(静默降级)。

16.6 验证 SearchStrategy.ANCESTORS

在有管理端口子上下文的应用里(第 25 章),给子上下文的配置类加 @ConditionalOnBean(name = "someMainContextBean", search = SearchStrategy.ANCESTORS): 主上下文有该 Bean 时生效;改成 CURRENT 则不生效。

16.7 验证 phase 校验

static class Bad extends AnyNestedCondition {
    Bad() { super(ConfigurationPhase.PARSE_CONFIGURATION); }
    @ConditionalOnMissingBean(Object.class)     // REGISTER_BEAN 阶段的条件
    static class Member { }
}

期望:启动抛 IllegalStateException: Nested condition ... uses a configuration phase that is inappropriate for ...。

16.8 验证预筛只记录否定

把 logging.level.org.springframework.boot.autoconfigure.condition=TRACE, 观察日志里只有 "did not match" 的条目来自 OnClassCondition 的预筛路径, "matched" 的条目来自正式求值路径。


17. 源码索引

主题文件关键行
模板基类condition/SpringBootCondition.java44–62(matches final)、52–58(NoClassDefFoundError)
双身份基类condition/FilteringSpringBootCondition.java52–69(match)、118–158(ClassNameFilter)、138–148(resolve)
类条件condition/OnClassCondition.java44–59(分半并行)、82–112(正式求值)
Bean 条件condition/OnBeanCondition.java91–94(phase)、97–113(预筛)、127–155(三注解顺序)
212–255(getMatchingBeans)、280–300(isCandidate)
561–604(Spec 构造)、674–732(类型推断)、806–828(SingleCandidateSpec)
833–914(MatchResult)、927–934(BeanType)
属性条件condition/OnPropertyCondition.java55–69(不短路)、166–187(collectProperties / isMatch)
列表属性条件condition/OnPropertyListCondition.java55–63(用 Binder)
Web 条件condition/OnWebApplicationCondition.java79–90(required 翻转)、118–147(四级判定)
嵌套条件condition/AbstractNestedCondition.java63–68、106–124(phase 校验)、156–180(getUltimateOutcome)
condition/AnyNestedCondition.java60–70
判定结果condition/ConditionOutcome.java100–103(getMessage 空返回 null)
报告condition/ConditionEvaluationReport.java152–172(get)、128–140(祖先传播)、183–199(getDelta)
filter 注册../resources/META-INF/spring.factories14–18
嵌套条件实例jdbc/autoconfigure/DataSourceAutoConfiguration.java93–108
grpc-client/autoconfigure/CompositeChannelFactoryAutoConfiguration.java55–71

18. 本章未能从本仓库核实的部分

项说明
SpringFactoriesLoader.loadFactories 是否排序属 Spring Framework 7.0.9,本仓库无源码。§4.1 给了反证推断(否则 @Order 是死代码),置信度高
ConditionEvaluator 是否对 Condition 排序同上。§14 图的第 4 层依赖这一点
ConfigurationClassParser 的两阶段调用细节Framework 行为;本章 §1.1 的描述基于 Boot 侧的 phase 语义反推
BeanDefinition#isDefaultCandidate / isFallbackFramework 6.2 新增,本仓库无源码;语义描述依据 Boot 的调用点推断
getMemberClassNames() 的返回顺序依赖 ASM 实现;§10.1 未对顺序做断言

19. 本章未逐行核实 / 待你实测的项

  1. 21 种 @ConditionalOn* 中,冷门注解的实现未逐个读。
  2. OnClassCondition 的 ASM 解析细节未展开。
  3. ConditionEvaluationReport 的完整格式在第 16 章。
  4. 条件在 AOT 下如何被固化为生成代码未逐行读(第 28 章)。
  5. 自定义条件选择 ConfigurationPhase 的边界未穷举。

20. 下一章

第 13 章讲了"原始 Bean 定义从哪来",第 14 章讲了"自动配置类怎么被选出并排序", 本章讲了"每个自动配置类怎么决定自己生不生效"。

还剩下最后一个问题:怎么知道结果? 也就是——

  • 配置元数据(spring-configuration-metadata.json)是怎么生成的、里面有什么
  • ConditionEvaluationReport 怎么变成 /actuator/conditions 的输出
  • AutoConfigurationSorter 之外的另一个排序维度:@AutoConfiguration 的 before/after 属性
  • 失败分析器(FailureAnalyzer)如何把条件不匹配翻译成人类可读的错误

第 16 章:排序、元数据与报告 —— AutoConfigurationSorter、配置元数据、条件报告与失败分析。