本节目标:讲清
@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_BEAN | Bean 定义注册时(更晚) | 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的AutoConfigurationImportListenerkey 下,负责把导入阶段(解析配置类之前)已经做出的批量过滤结论也并进报告,所以--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 自动配置的覆盖与排除 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。