本文是 Spring Boot 4 系列第 11 篇 | 基于 Spring Boot 4.1.0 GA 源码 + Spring Framework 7.0.x + JDK 17 基线 | 预计阅读 35 分钟
文末附「全链路总结图」和「三大核心设计思想」,写自定义 starter 或排查自动配置失效时可直接对照。
写在前面
pom.xml 里加一个 spring-boot-starter-web,一行配置不写,应用启动后自动有了 DispatcherServlet、RequestMappingHandlerAdapter、内嵌 Tomcat、JSON 序列化器。往容器里手动注册一个自己的 ObjectMapper,Jackson 自动配置的 @ConditionalOnMissingBean 会判定你已定义,直接退让。
这两件事背后是同一套机制:自动配置(Auto-configuration)。
这篇基于 Spring Boot 4.1.0 GA 源码,从 @EnableAutoConfiguration 注解出发,把自动配置从「注解入口 → 候选加载 → 两道过滤 → 拓扑排序 → Bean 注册」的完整链路走一遍,重点回答五个问题:
- 候选类列表是从哪里加载的?为什么 Spring Boot 4 不再用
spring.factories? - 上百个自动配置类,JVM 是怎么做到不加载类字节码就快速剔除掉大部分候选的?
@ConditionalOnClass/@ConditionalOnBean/@ConditionalOnWebApplication的底层判定逻辑到底是什么?- 自动配置类的执行顺序是怎么排出来的?
- AOT 模式下,这些条件评估为什么被搬到了构建期?
内容速览
- 自动配置全景图:从 @EnableAutoConfiguration 到 Bean 注册的完整链路
- 入口机制:DeferredImportSelector 为什么是"退让"的根基
- 核心管线:getAutoConfigurationEntry() 的 8 步流程
- 候选加载:从 spring.factories 到 AutoConfiguration.imports,ImportCandidates 的实现
- 4.x 模块化自动配置:128 个模块、99 个 imports 文件、AutoConfigurationReplacements 迁移垫片
- 第一道过滤:编译期元数据 + Class.forName,不加载字节码剔除候选
- 第二道评估:ConditionEvaluator 两阶段,OnClassCondition / OnBeanCondition / OnWebApplicationCondition 的判定细节
- 排序:字母序 → @AutoConfigureOrder → 拓扑排序 + 环检测
- 排错利器:ConditionEvaluationReport 与 --debug 报告、Actuator conditions 端点
- AOT 架构:基础设施下沉 Spring Framework 7,条件评估搬到构建期
- 实战:手写一个 starter,断点跟踪条件评估全过程
一、自动配置全景图
先看一张总览图(建议右键新标签页打开):
less
@SpringBootApplication (组合注解)
└─ @EnableAutoConfiguration
└─ @Import(AutoConfigurationImportSelector.class) ← 入口:延迟导入选择器
│
└─ ConfigurationClassParser(Spring Framework)按 DeferredImportSelector 语义,
在「所有用户 @Configuration 处理完之后」才调用:
└─ AutoConfigurationImportSelector.selectImports()
└─ getAutoConfigurationEntry() ← 核心管线
├─ [1] getAttributes() 读取 exclude/excludeName 属性
├─ [2] getCandidateConfigurations() ImportCandidates 扫描
│ └─ META-INF/spring/<注解全名>.imports ← 每个 jar 一个文件,合并
├─ [3] removeDuplicates() 去重
├─ [4] getExclusions() 合并注解排除 + spring.autoconfigure.exclude 属性
├─ [5] checkExcludedClasses() 校验排除项合法性
├─ [6] configurations.removeAll(exclusions) 应用排除
├─ [7] getConfigurationClassFilter().filter() ★ 第一道过滤(不加载字节码)
│ ├─ OnClassCondition ← 按 classpath 类存在性过滤
│ ├─ OnBeanCondition ← 按条件涉及的类存在性过滤(Bean 判定留到注册阶段)
│ └─ OnWebApplicationCondition
└─ [8] fireAutoConfigurationImportEvents() 发布导入事件
└─ AutoConfigurationGroup.selectImports()
└─ AutoConfigurationSorter.getInPriorityOrder() ★ 排序
├─ ① 字母序
├─ ② @AutoConfigureOrder
└─ ③ @AutoConfigureBefore / @AutoConfigureAfter 拓扑排序
│
└─ 过滤后的自动配置类以 @Configuration 身份进入标准 Bean 加载流程
└─ ConditionEvaluator.shouldSkip() ★ 第二道(完整)条件评估
├─ PARSE_CONFIGURATION 阶段: @ConditionalOnClass / OnWebApplicationCondition 等
└─ REGISTER_BEAN 阶段: @ConditionalOnBean / @ConditionalOnMissingBean
└─ SpringBootCondition.matches() 模板方法
├─ getMatchOutcome() ← 子类实现判定逻辑
├─ logOutcome() ← 输出 "Condition ... matched/did not match"
└─ recordEvaluation() ← 写入 ConditionEvaluationReport(--debug 报告)
整个机制可以总结成一句话:"扫描候选 → 不加载类快速过滤 → 完整条件评估 → 拓扑排序 → 注册为普通 @Configuration"。
下面逐层拆开。
二、入口:@EnableAutoConfiguration 与 DeferredImportSelector
2.1 注解本体
@SpringBootApplication 是一个组合注解,其中就组合了 @EnableAutoConfiguration。在 4.1.0 中它的定义位于:
core/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/EnableAutoConfiguration.java
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// exclude() 按类排除;excludeName() 按类名排除(String,可加载不存在的类)
}
两个关键点:
@AutoConfigurationPackage:把主类所在包注册为"自动配置包",供AutoConfigurationPackages查询(@Entity扫描、MyBatis mapper 扫描等都会用到它)。底层由AutoConfigurationPackages.Registrar(core/spring-boot-autoconfigure/.../AutoConfigurationPackages.java:129)注册一个名为org.springframework.boot.autoconfigure.AutoConfigurationPackages的基础设施 Bean。@Import(AutoConfigurationImportSelector.class):把自动配置的"挑选逻辑"交给一个DeferredImportSelector。
2.2 为什么必须是 DeferredImportSelector?
AutoConfigurationImportSelector 的类声明(core/spring-boot-autoconfigure/.../AutoConfigurationImportSelector.java:78):
java
public class AutoConfigurationImportSelector implements DeferredImportSelector,
BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
DeferredImportSelector 是 Spring Framework 7 中 ImportSelector 的一个子接口,它的语义是:延迟到所有普通 @Configuration 类都处理完之后,再处理这批导入。
这解释了为什么用户手动定义的 Bean 总能赢过自动配置:用户自己的配置类在 ConfigurationClassParser 的第一轮就解析完了,而自动配置的导入被推迟到最后一轮。先注册的 Bean 定义,让 @ConditionalOnMissingBean 有机会"看到"它们并主动退让。这正对应 @EnableAutoConfiguration javadoc 里那句话:
Auto-configuration is always applied after user-defined beans have been registered.
延迟导入发生在任何条件判断之前,是自动配置能"退让"的前提。
2.3 触发时机:Spring Framework 的 ConfigurationClassParser
selectImports() 是在谁的手里被调用的?答案是 Spring Framework 7 的 ConfigurationClassParser。refresh() → invokeBeanFactoryPostProcessors() → ConfigurationClassPostProcessor → ConfigurationClassParser.parse(),流程大致是:
- 解析所有候选配置类(主类、@Component 扫描结果、@Import 等);
- 遇到
DeferredImportSelector,不立即处理 ,先放进deferredImportSelectorHandler队列; - 所有普通配置类解析完毕后,再调用
deferredImportSelectorHandler.process(); process()里按getImportGroup()分组,每个 Group 先process()(收集数据)、再selectImports()(产出结果)------这就是DeferredImportSelector.Group接口的语义。
AutoConfigurationImportSelector.getImportGroup() 返回 AutoConfigurationGroup.class(AutoConfigurationImportSelector.java:158),这是 Boot 的一个内部类,后面会看到它的作用。
三、核心管线:getAutoConfigurationEntry()
当 Group 被驱动时,AutoConfigurationImportSelector.selectImports() 调用了核心方法 getAutoConfigurationEntry()(AutoConfigurationImportSelector.java:142):
java
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
8 步管线,职责清晰:
| 步骤 | 方法 | 职责 |
|---|---|---|
| 1 | isEnabled() |
检查环境属性 spring.boot.enableautoconfiguration(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY)是否为 false,默认启用(AutoConfigurationImportSelector.java:162) |
| 2 | getAttributes() |
读取注解上的 exclude / excludeName 属性 |
| 3 | getCandidateConfigurations() |
加载所有候选自动配置类(下一节详解) |
| 4 | removeDuplicates() |
去重(多 jar 声明同名类时只保留一个) |
| 5 | getExclusions() |
合并「注解排除」+「spring.autoconfigure.exclude 属性排除」 |
| 6 | checkExcludedClasses() |
校验排除项是否真实存在,防止写错类名 |
| 7 | getConfigurationClassFilter().filter() |
第一道过滤:不加载类字节码,仅凭元数据快速剔除 |
| 8 | fireAutoConfigurationImportEvents() |
发布 AutoConfigurationImportEvent,通知监听器 |
注意第 8 步------过滤之后才发事件。ConditionEvaluationReportAutoConfigurationImportListener(通过 spring.factories 注册为 AutoConfigurationImportListener)会在这里把候选类和排除项记入 ConditionEvaluationReport,这是 --debug 报告的数据来源之一,后面再展开。
四、候选类从哪来:从 spring.factories 到 AutoConfiguration.imports
4.1 历史演进
| 版本 | 机制 | 说明 |
|---|---|---|
| Boot 1.x ~ 2.6 | META-INF/spring.factories |
以 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 为 key 声明自动配置类列表 |
| Boot 2.7 | 引入 AutoConfiguration.imports |
新机制与 spring.factories 双轨并行,@AutoConfiguration 注解登场(2.7 起) |
| Boot 3.0 | 全面切换 | 自动配置读取只走 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,spring.factories 中对应条目被移除 |
| Boot 4.x | 彻底告别 | spring.factories 中已无任何 EnableAutoConfiguration 条目(本仓库全量 grep 为 0),连"兼容读取"都不存在了 |
为什么换掉 spring.factories?三个原因:
- 性能 :
spring.factories是 properties 格式,SpringFactoriesLoader需要把整个文件解析成 Map;而.imports每行一个类名,加载更快。 - 可读性 :
.imports文件就是一个纯列表,Git diff 更友好,review 更容易。 - 职责分离 :
spring.factories承担了大量 SPI 注册职责(EnvironmentPostProcessor、ApplicationListener、FailureAnalyzer等),把"自动配置列表"从中剥离,两种机制的演进互不拖累。
一个容易搞错的点:Spring Boot 4 并没有废除 spring.factories 机制本身 。本仓库中仍有 70+ 个 spring.factories 文件在正常工作------只是"自动配置类列表"这一项职责被移走了。比如 core/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories 里仍然注册着 ApplicationContextInitializer、ApplicationListener、AutoConfigurationImportFilter、AutoConfigurationImportListener、FailureAnalyzer 等 SPI。所以准确的说法是:自动配置的声明方式变了,SPI 机制没变。
4.2 4.1.0 中的加载实现:ImportCandidates
读取 .imports 文件的不是 Boot 的 SpringFactoriesLoader,而是 ImportCandidates(位于 core/spring-boot/src/main/java/org/springframework/boot/context/annotation/ImportCandidates.java)。
AutoConfigurationImportSelector.getCandidateConfigurations()(AutoConfigurationImportSelector.java:200):
java
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 注意:默认读的是 AutoConfiguration.class 的全名,不是 EnableAutoConfiguration!
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, getBeanClassLoader());
List<String> configurations = importCandidates.getCandidates();
Assert.state(!CollectionUtils.isEmpty(configurations),
"No auto configuration classes found in " + "META-INF/spring/"
+ this.autoConfigurationAnnotation.getName() + ".imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
ImportCandidates.load() 的机制(ImportCandidates.java:81):
java
public static ImportCandidates load(Class<?> annotation, @Nullable ClassLoader classLoader) {
...
String location = String.format(LOCATION, annotation.getName()); // LOCATION = "META-INF/spring/%s.imports"
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location); // classLoader.getResources(location)
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url)); // 逐行读取、去注释、trim
}
return new ImportCandidates(importCandidates);
}
两个细节值得注意:
classLoader.getResources(location):按资源路径扫描整个 classpath,每个 jar 里的同名文件都会被读到并合并 。这正是模块化自动配置的基础------每个模块自带一份.imports文件,谁在 classpath 上,谁的自动配置就生效。- 文件格式 :每行一个类全名,
#开头是注释。比如core/spring-boot-autoconfigure自己的 imports 文件前几行:
properties
# core/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
...
4.3 4.x 的模块化自动配置:核心瘦身 + 模块自治
这是 Spring Boot 4 在自动配置领域最重要的结构性变化。对比一下:
Spring Boot 3.x :几乎所有自动配置都堆在 spring-boot-autoconfigure 一个模块里,一个 imports 文件一百多行,模块重达几 MB,哪怕只用 JSON 序列化,也要引入整个 autoconfigure 模块。
Spring Boot 4.x :仓库重构为 core/(8 个核心模块) + module/(128 个模块 )两层结构,自动配置被拆散到各个功能模块中,每个模块在自己的 jar 里声明自己的 imports 文件:
bash
core/spring-boot-autoconfigure/src/main/resources/META-INF/spring/...AutoConfiguration.imports ← 仅 12 行(核心基建)
module/spring-boot-jackson/src/main/resources/META-INF/spring/...AutoConfiguration.imports ← 1 行:JacksonAutoConfiguration
module/spring-boot-gson/src/main/resources/META-INF/spring/...AutoConfiguration.imports ← 1 行:GsonAutoConfiguration
module/spring-boot-webmvc/src/main/resources/META-INF/spring/...AutoConfiguration.imports ← 6 行:DispatcherServlet 等
...
整个仓库共有 99 个 AutoConfiguration.imports 文件,共同构成全部候选。同时包名也改为「按特性分组」的新体系:org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration、org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration......(老的 org.springframework.boot.autoconfigure.* 包名下只保留 admin / aop / availability / cache / condition / context / data / jmx / ssl / task 等核心子包,webmvc、jackson 等大功能全部搬走;另有 spring-boot-autoconfigure-classic 等兼容模块供迁移期使用。)
加载逻辑没有变化。AutoConfigurationImportSelector 是通用的------它不看模块、不分组,只是通过 ImportCandidates 把 classpath 上所有 jar 的 imports 文件全部合并。模块化对 selector 完全透明:拆分只发生在声明层面,运行机制零改动。
4.4 4.x 起引入:AutoConfigurationReplacements
spring-boot-autoconfigure 模块根包里还有一个独立类 AutoConfigurationReplacements(4.0 开发期引入,提交信息为 "Provide support for deprecated auto-configuration classes"):
java
// AutoConfigurationReplacements.java
final class AutoConfigurationReplacements {
private static final String LOCATION = "META-INF/spring/%s.replacements";
...
}
它的作用(javadoc 原话):处理那些已被废弃或移动的自动配置类 ------比如老配置里 @AutoConfigureBefore / @AutoConfigureAfter / 排除项仍引用旧类名,通过 .replacements 文件把旧类名映射到新类名。AutoConfigurationImportSelector 中对应字段在 L107,懒加载逻辑在 L296-300;getExclusions()(L247)末尾会执行 getAutoConfigurationReplacements().replaceAll(excluded),排序阶段(AutoConfigurationSorter)也会套用同样的替换。这是 4.x 模块化重构后,为了保证老配置(spring.autoconfigure.exclude、老 imports)不因类名搬迁而失效而设计的"平滑迁移垫片"。
五、第一道过滤:不加载字节码,先剔除大部分候选
5.1 为什么需要"提前过滤"?
候选自动配置类可能有上百个,如果全部交给 Spring 的配置类解析器(它会用 ASM 读每个类的字节码、解析注解),开销可观。Boot 的设计是:在真正读取类字节码之前,先用最廉价的手段(类存在性检查 + 编译期生成的元数据)把明显不匹配的候选剔除。
这就是 AutoConfigurationImportFilter 接口的用途(core/spring-boot-autoconfigure/.../AutoConfigurationImportFilter.java:59):
java
@FunctionalInterface
public interface AutoConfigurationImportFilter {
boolean[] match(@Nullable String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata);
}
- 入参:全部候选类名数组 +
AutoConfigurationMetadata(编译期生成的元数据); - 返回值:与入参等长的
boolean[],false的候选直接被丢弃; - 语义(javadoc 原文):"This interface is designed to allow fast removal of auto-configuration classes before their bytecode is even read"------在读取字节码之前快速移除自动配置类。
5.2 过滤器从哪来?还是 spring.factories
过滤器本身通过 SpringFactoriesLoader.loadFactories(AutoConfigurationImportFilter.class, ...) 加载(AutoConfigurationImportSelector.java:279),注册位置在 spring-boot-autoconfigure 的 spring.factories:
properties
# core/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
三个过滤器正是三个最常用的条件类,同时承担两个职责:既是 AutoConfigurationImportFilter(提前过滤),又是 Condition(最终评估) ,统一在抽象类 FilteringSpringBootCondition 里:
java
// core/spring-boot-autoconfigure/.../condition/FilteringSpringBootCondition.java:42
abstract class FilteringSpringBootCondition extends SpringBootCondition
implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {
@Override
public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
boolean[] match = new boolean[outcomes.length];
for (int i = 0; i < outcomes.length; i++) {
match[i] = (outcomes[i] == null || outcomes[i].isMatch());
if (!match[i]) {
// 不匹配的写入 ConditionEvaluationReport
...
}
}
return match;
}
...
}
注意 match[i] = (outcomes[i] == null || outcomes[i].isMatch()):返回 null 表示"现在判断不了,放行,后面再仔细判"。
5.3 元数据从哪来:编译期生成的 spring-autoconfigure-metadata.properties
AutoConfigurationMetadata 的默认实现来自 AutoConfigurationMetadataLoader(core/spring-boot-autoconfigure/.../AutoConfigurationMetadataLoader.java:38):
java
private static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
这个文件不是手写的 ,而是编译期由注解处理器生成的。处理器位于 core/spring-boot-autoconfigure-processor/,名叫 AutoConfigureAnnotationProcessor,它的 @SupportedAnnotationTypes 明确声明了处理 8 个注解:
java
@SupportedAnnotationTypes({
"org.springframework.boot.autoconfigure.condition.ConditionalOnClass",
"org.springframework.boot.autoconfigure.condition.ConditionalOnBean",
"org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate",
"org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication",
"org.springframework.boot.autoconfigure.AutoConfigureBefore",
"org.springframework.boot.autoconfigure.AutoConfigureAfter",
"org.springframework.boot.autoconfigure.AutoConfigureOrder",
"org.springframework.boot.autoconfigure.AutoConfiguration" })
它把每个自动配置类上的条件注解编译期就静态分析出来 ,写入 spring-autoconfigure-metadata.properties,格式大致是:
properties
# 生成的格式示意(运行时按 key 读取)
com.example.GreetingAutoConfiguration.ConditionalOnClass=com.example.sdk.GreetingClient
com.example.GreetingAutoConfiguration.AutoConfigureAfter=org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration
有了这份"预计算"的元数据,过滤阶段查一个条件只是 Map 查找 + 类名判存在,完全不碰字节码。
5.4 OnClassCondition 的过滤路径:并行 + 兜底
OnClassCondition 是三个过滤器中 @Order(Ordered.HIGHEST_PRECEDENCE) 的(OnClassCondition.java:45),最先执行。它的 getOutcomes() 做了一个并行优化(OnClassCondition.java:49):
java
@Override
protected final @Nullable ConditionOutcome[] getOutcomes(@Nullable String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
// 候选多于 1 个且机器多于 1 核时,拆成两半,一半放后台线程并行判定
if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) {
return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata);
}
...
}
resolveOutcomesThreaded() 把数组从中间拆开,前半段交给 ThreadedOutcomesResolver(内部新起一个线程),后半段主线程直接算,再 System.arraycopy 拼回结果。注释说明:只多开一个线程,再多反而变慢。
真正的判定在 StandardOutcomesResolver(OnClassCondition.java:183):
java
// 从编译期元数据里取这个自动配置类的 @ConditionalOnClass 候选类名列表
List<String> candidates = autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnClass");
...
try {
outcome = getOutcome(candidates); // 逐个 Class.forName 判存在性
} catch (Exception ex) {
// 抛异常时返回 null ------ 注释:'We'll get another chance later'
}
注释 "We'll get another chance later" 是关键:过滤阶段只做类存在性这类廉价判断,如果判定过程抛异常(比如类加载器边界问题),返回 null 放行,交给完整的条件评估阶段再判一次------后面还有一道关。
5.5 ClassNameFilter:比 ClassUtils.forName 更快的存在性检查
FilteringSpringBootCondition 里定义了 ClassNameFilter 枚举(PRESENT / MISSING):
java
// FilteringSpringBootCondition.java:115
static Class<?> resolve(String className, ClassLoader classLoader) {
// 用 Class.forName(className, false, ...):不初始化类!比 ClassUtils.forName 快
return Class.forName(className, false, classLoader);
}
两个细节:
initialize = false:只加载不初始化,避免触发 static 块,更快更安全;ClassNameFilter.isPresent内部 catch 一切 Throwable 返回 false------不因类加载异常把整个启动打崩。
经过这一轮过滤,大批候选(比如 classpath 上根本没有对应 SDK 的那些)被 false 掉,剩下的才进入真正的条件评估。
六、第二道关:Condition 接口与 SpringBootCondition 模板方法
6.1 条件评估发生在哪两个阶段?
过滤后的自动配置类被作为 @Configuration 类重新进入 Spring Framework 的标准解析流程。此时真正的评估者是 Spring Framework 的 ConditionEvaluator(org.springframework.context.annotation.ConditionEvaluator),它在两个阶段各评估一次:
| 阶段 | 时机 | 评估对象 |
|---|---|---|
PARSE_CONFIGURATION |
配置类解析时 | 类级条件(@ConditionalOnClass、@ConditionalOnWebApplication 等) |
REGISTER_BEAN |
Bean 定义注册时 | 方法级条件 + 需要"看已注册 Bean"的条件(@ConditionalOnBean、@ConditionalOnMissingBean) |
阶段语义由 Spring Framework 的 ConfigurationCondition 接口给出:
java
public interface ConfigurationCondition extends Condition {
ConfigurationPhase getConfigurationPhase();
enum ConfigurationPhase { PARSE_CONFIGURATION, REGISTER_BEAN }
}
为什么 @ConditionalOnBean 必须在 REGISTER_BEAN 阶段评估? 因为"某个 Bean 是否存在"取决于注册顺序------如果还处于解析阶段,后面 @Bean 方法可能还没注册,判断结果会不准。Boot 里实现 ConfigurationCondition 的有两个:OnBeanCondition(OnBeanCondition.java:88-89,返回 REGISTER_BEAN)和 AbstractNestedCondition(AbstractNestedCondition.java:47,phase 由构造参数指定,是 AnyNestedCondition / AllNestedConditions / NoneNestedConditions 的公共父类);其余条件(OnClassCondition、OnWebApplicationCondition 等)都按默认的 PARSE_CONFIGURATION 阶段评估。
6.2 SpringBootCondition:模板方法模式
所有 Boot 条件都继承 SpringBootCondition(core/spring-boot-autoconfigure/.../condition/SpringBootCondition.java:39):
java
public abstract class SpringBootCondition implements Condition {
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
String classOrMethodName = getName(metadata);
try {
ConditionOutcome outcome = getMatchOutcome(context, metadata); // ★ 子类实现
logOutcome(classOrMethodName, outcome); // 输出启动日志
recordEvaluation(context, classOrMethodName, outcome); // 写入 ConditionEvaluationReport
return outcome.isMatch();
}
catch (NoClassDefFoundError ex) {
throw new IllegalStateException(...);
}
catch (RuntimeException ex) {
throw new IllegalStateException(...);
}
}
public abstract ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata);
}
matches()是 final 的,固定了"判定 → 日志 → 记录"三段式流程;- 子类只需实现
getMatchOutcome(),返回ConditionOutcome(是否匹配 +ConditionMessage说明); NoClassDefFoundError/RuntimeException会被包装成IllegalStateException------条件判定失败绝不能静默吞掉。
6.3 OnClassCondition:类存在性判定
java
@Order(Ordered.HIGHEST_PRECEDENCE)
class OnClassCondition extends FilteringSpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ClassLoader classLoader = context.getClassLoader();
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)); // ← 这就是日志里 "did not find required classes" 的来源
}
...
}
// 对 @ConditionalOnMissingClass 同理,但语义反转:存在"不该存在的类" → noMatch
...
}
}
关键细节在 getCandidates()(OnClassCondition.java:117):
java
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(annotationType.getName(), true);
// 注意第二个参数 classValuesAsString = true!
// 把 @ConditionalOnClass(value = X.class) 的 Class 字面量统一读成字符串类名
为什么这里能拿到字符串类名? 因为 AnnotatedTypeMetadata 是 Spring Framework 的 MetadataReader(ASM 读取字节码)产生的------注解里的 Class 字面量在 class 文件注解属性中就是以类名的字符串形式存储,ASM 读出它根本不需要加载这个类 。这就是 @ConditionalOnClass javadoc 里那句 "parsed by using ASM before the class is loaded" 的含义(原文:"A {@code Class} value can be safely specified on @Configuration classes as the annotation metadata is parsed by using ASM before the class is loaded" ),也是整个自动配置体系敢写 @ConditionalOnClass(某个不存在的类.class) 而不崩的根基。
另外澄清一个常见误解:OnClassCondition 本身没有任何 ASM 代码 。ASM 读注解发生在 Spring Framework 的 ConfigurationClassParser / MetadataReader 层;Boot 侧只是消费 ASM 解析出来的元数据字符串。本仓库中直接调用 MetadataReader 读类字节码的位置有三处:AbstractNestedCondition.java:149(嵌套成员条件的元注解读取)、AutoConfigurationSorter.java:284(AutoConfigurationClass.getAnnotationMetadata(),见第七节)、NoSuchBeanDefinitionFailureAnalyzer.java:233(启动失败诊断)------其中排序阶段读字节码是"元数据缺失时的兜底",这也正反衬出第一道过滤刻意避免读字节码的性能价值。
6.4 OnBeanCondition:BeanDefinition 层面的判定
java
@Order(Ordered.LOWEST_PRECEDENCE) // L88;三个过滤器里它最后跑
class OnBeanCondition extends FilteringSpringBootCondition implements ConfigurationCondition { // L89
@Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN; // ★ 必须等 Bean 注册阶段
}
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 依次评估:@ConditionalOnBean → @ConditionalOnSingleCandidate → @ConditionalOnMissingBean
// 任一不匹配,直接短路返回 noMatch
...
}
}
真正干活的 getMatchingBeans()(OnBeanCondition.java:212)在 ConfigurableListableBeanFactory 上按三条路找 Bean:
- 按类型 (
type):内部通过getBeanNamesForType(...)取类型匹配的 Bean 定义(getBeanDefinitionsForType包装),支持parameterizedContainer(泛型容器,如@ConditionalOnBean(parameterizedContainer = List.class)匹配List<Foo>); - 按注解 (
annotation):扫描 Bean 定义上带指定注解的 Bean; - 按名称 (
name)。
并支持 SearchStrategy(CURRENT / ANCESTORS / ALL)、ignoredTypes 忽略名单、scoped proxy 判断等。注意它匹配的是已注册的 BeanDefinition (beanFactory.getBeanNamesForType 不实例化 Bean),所以"手动定义 ObjectMapper → Jackson 自动配置 @ConditionalOnMissingBean 判定存在 → 主动退让"这条链路就发生在这一步。
6.5 OnWebApplicationContext:探针类判定
OnWebApplicationCondition(OnWebApplicationCondition.java:47-48,@Order 在 L47、类声明在 L48,值为 HIGHEST_PRECEDENCE + 20)不直接看容器类型,而是用探针类(probe classes)判断:
java
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";
isServletWebApplication()(OnWebApplicationCondition.java:128)依次检查:探针类是否存在 → 是否有 session scope → Environment 是否是 ConfigurableWebEnvironment → resource loader 是否是 WebApplicationContext;isReactiveWebApplication()(L148)同理。required 标志(L93,metadata.isAnnotated(...))让同一个条件类同时服务 @ConditionalOnWebApplication(需要 Web 环境)和 @ConditionalOnNotWebApplication(需要非 Web 环境)两个注解。
另外 @ConditionalOnWebApplication 并没有 matchIfMissing 属性------它"缺省匹配"的语义来自 type() default Type.ANY:deduceType() 取不到 type 时返回 Type.ANY(Servlet 或 Reactive 任一匹配即可)。
6.6 ConditionMessage:日志是怎么生成的
每个 ConditionOutcome 都携带一个 ConditionMessage。它的 Builder API 就是你在启动日志里看到的那些句子的来源:
dart
Condition OnClassCondition on com.example.GreetingAutoConfiguration matched
- @ConditionalOnClass found required class 'com.example.sdk.GreetingClient' (OnClassCondition)
ConditionMessage.forCondition(ConditionalOnClass.class).found("required class", ...).items(Style.QUOTE, ...) 攒出消息体,多个条件用 "; " 连接,最后随 ConditionOutcome 被 logOutcome 打印、被 recordEvaluation 写进报告。
七、排序:AutoConfigurationSorter 的三步走
过滤完之后,候选集还会排序 ------顺序很重要,比如 ServletWebServerFactoryAutoConfiguration 必须排在 DispatcherServletAutoConfiguration 之前。排序逻辑在 AutoConfigurationSorter.getInPriorityOrder()(core/spring-boot-autoconfigure/.../AutoConfigurationSorter.java:65,注意 4.x 中这个类是包私有的,3.x 里还是 public):
java
List<AutoConfigurationClass> getInPriorityOrder(Collection<String> classNames) {
List<AutoConfigurationClass> orderedClasses = ...;
// ① 按类名字母序排序
...
// ② 按 @AutoConfigureOrder 排序(数值小的在前)
orderedClasses.sort((c1, c2) -> Integer.compare(c1.getOrder(), c2.getOrder()));
// ③ 按 @AutoConfigureBefore / @AutoConfigureAfter 做拓扑排序
doSortByAfterAnnotation(orderedClasses);
checkForCycles(orderedClasses);
return orderedClasses;
}
三个细节:
- 字母序:保证排序的确定性,同权重的类按类名字典序排;
@AutoConfigureOrder:读 order 时优先读编译期元数据 (AutoConfigurationClass.getOrder(),AutoConfigurationSorter.java:243,key 为"AutoConfigureOrder"),读不到再反射读注解value,默认AutoConfigureOrder.DEFAULT_ORDER = 0;- 拓扑排序 :
@AutoConfigureBefore/@AutoConfigureAfter构成有向边,doSortByAfterAnnotation做拓扑排序;类名解析同样优先走元数据 (getClassNames(),L222,key 为"AutoConfigureBefore"/"AutoConfigureAfter"),并会套用AutoConfigurationReplacements做新旧类名替换;最后checkForCycles(L114)检测环,存在环直接抛异常:
sql
AutoConfigure cycle detected between com.example.A and com.example.B
4.x 的 @AutoConfiguration 注解自身就元注解了 @Configuration(proxyBeanMethods = false)、@AutoConfigureBefore 和 @AutoConfigureAfter(AutoConfiguration.java:58-61),并提供了 before() / beforeName() / after() / afterName() 四个别名属性------写自动配置时不用再叠两个注解:
java
@AutoConfiguration(before = SomeOtherAutoConfiguration.class) // 等价于 @AutoConfigureBefore(SomeOtherAutoConfiguration.class)
八、排错利器:ConditionEvaluationReport 与 --debug
条件评估的每一步结果都被记入 ConditionEvaluationReport(core/spring-boot-autoconfigure/.../condition/ConditionEvaluationReport.java):
- 它是个注册在 BeanFactory 里、名为
"autoConfigurationReport"的单例 Bean(L55); - 内部用
SortedMap<String, ConditionAndOutcomes>(L59,TreeMap实现)按配置类名记录每个条件的结果; - 写入方:
SpringBootCondition.recordEvaluation()(SpringBootCondition.java:102)+FilteringSpringBootCondition.match()的过滤不匹配记录 +ConditionEvaluationReportAutoConfigurationImportListener(记录候选和排除项,recordExclusions在L95、recordEvaluationCandidates在L105)。
谁消费它?ConditionEvaluationReportLoggingListener(通过 spring.factories 注册为 ApplicationContextInitializer)。当启动失败时,它会自动打印完整报告;应用正常启动时,加 --debug 参数就能看到:
markdown
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches:
-----------------
AopAutoConfiguration matched:
- @ConditionalOnClass found required class
'org.springframework.aop.config.AopConfigUtils' (OnClassCondition)
Negative matches:
-----------------
ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required class
'javax.jms.ConnectionFactory' (OnClassCondition)
Exclusions:
-----------
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
(以上为运行 --debug 时的真实报告格式节选,Positive/Negative 示例类参照官方文档 CONDITIONS EVALUATION REPORT 示例,具体类名以你的 classpath 为准。)这份报告是排查"为什么某个自动配置没生效"的第一手依据。生产环境还能通过 Actuator 的 conditions 端点远程查看(注意:4.x 中该端点实现类由 3.x 的 ConditionsEndpoint 更名为 ConditionsReportEndpoint,端点 ID conditions 保持不变)。
九、AOT 模式:把条件评估搬到构建期
9.1 4.x 的重大架构变化:AOT 基础设施进了 Spring Framework
3.x 里这些类都在 Spring Boot 自己的 org.springframework.boot.aot 包里:AotProcessor、AotApplicationContext、AutoConfigurationAotProcessor、GeneratedFactory......在 Spring Boot 4 里它们全部消失了。
4.x 把整套 AOT 基础设施下沉到了 Spring Framework 7:
| 3.x(Spring Boot 自带) | 4.x(Spring Framework 7) |
|---|---|
org.springframework.boot.aot.AotProcessor |
org.springframework.context.aot.ContextAotProcessor(spring-context) |
AotApplicationContext |
GenericApplicationContext.refreshForAotProcessing() |
AutoConfigurationAotProcessor / ImportAutoConfigurationAotProcessor |
已删除,统一走 BeanRegistrationsAotProcessor(spring-beans) |
GeneratedFactory / GeneratedBean / CodeGenerator |
org.springframework.aot.generate.GeneratedClass / GeneratedMethods(spring-core) |
@AotGenerated 注解(org.springframework.aot.generated.annotation) |
消失;生成类统一由 GeneratedClass 自动加注 org.springframework.aot.generate.Generated |
Spring Boot 侧只剩下一个薄入口 SpringApplicationAotProcessor(core/spring-boot/src/main/java/org/springframework/boot/SpringApplicationAotProcessor.java:43),直接继承 Framework 的 ContextAotProcessor。
9.2 构建期如何执行一遍启动流程
AOT 处理在构建期执行(Gradle 的 bootProcessAot 任务------任务类型 ProcessAot,以及 Maven 的 process-aot 目标,两者的处理器类名都指向 org.springframework.boot.SpringApplicationAotProcessor)。它的做法是把应用的 main 方法真正跑起来,在特定时机中断:
java
// SpringApplicationAotProcessor.java:103 ------ AotProcessorHook
// 注册一个 SpringApplicationRunListener,
// 在 contextLoaded 回调时抛出 AbandonedRunException(context) 提前终止 main,
// 从而捕获一个已构建完成的 GenericApplicationContext
跑完 main 拿到上下文后,关键一步是 GenericApplicationContext.refreshForAotProcessing(runtimeHints)(spring-context),它和完整 refresh() 几乎一样走一遍 invokeBeanFactoryPostProcessors()------AutoConfigurationImportSelector 就在这里被触发,所有条件评估、过滤、排序都在构建期完整执行了一遍 ,然后 freezeConfiguration() 冻结 BeanDefinition。这保证了 AOT 生成的代码与运行时真实结果一致。
之后 ApplicationContextInitializationCodeGenerator 生成一个 <MainClass>__ApplicationContextInitializer 类,BeanRegistrationsAotProcessor 遍历 beanFactory.getBeanDefinitionNames() 为每个 Bean 生成注册代码------自动配置贡献的 Bean 和用户 Bean 一视同仁 ,不再有专门的自动配置 AOT 处理器。生成代码的发现机制是 AotServices.factoriesAndBeans() 读取各模块的 META-INF/spring/aot.factories + 容器内 Bean。spring-boot-autoconfigure 自己的 aot.factories 里注册了:
properties
# core/spring-boot-autoconfigure/src/main/resources/META-INF/spring/aot.factories
org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingProcessor
org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer
org.springframework.aot.hint.RuntimeHintsRegistrar=\
org.springframework.boot.autoconfigure.template.TemplateRuntimeHints
注意 ConditionEvaluationReportLoggingProcessor------构建期也会生成条件评估报告,如果某个自动配置在 AOT 构建期被条件拒绝,你能在构建日志里直接看到原因。
9.3 运行期如何切换到生成代码
打包后的应用运行期由 SpringApplication 判断(core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java):
java
// L412:AOT 模式下跳过源码加载
if (!AotDetector.useGeneratedArtifacts()) { ... }
// L421 addAotGeneratedInitializerIfNecessary(...):
// AOT 模式下按 <mainClass>__ApplicationContextInitializer 类名加载生成的 Initializer,
// 用 AotApplicationContextInitializer.forInitializerClasses(...) 包装后置顶执行
找不到生成类时抛 AotInitializerNotFoundException(并有配套的 AotInitializerNotFoundFailureAnalyzer 给出友好诊断)。
带来的变化是:启动期不再需要扫描 classpath、不再需要动态评估条件------启动链路变成"执行生成代码直接注册 Bean",这就是原生镜像 / AOT 应用启动毫秒级的根本原因之一。
十、实战:手写一个自定义自动配置,并跟踪条件评估全过程
理论拆完,动手写一个自动配置,用断点走完全链路。
10.1 场景
假设你的公司有一个内部 SDK com.example:greeting-sdk,提供 GreetingClient(访问远程问候服务)。你想做一个 starter:classpath 上有 GreetingClient 时自动配好 GreetingService,且用户自定义了 GreetingService 时必须退让。
10.2 三步:自动配置类 + imports 文件 + 元数据处理器
① 自动配置类 (新写法,用 @AutoConfiguration 而不是 @Configuration):
java
// com/example/greeting/autoconfigure/GreetingAutoConfiguration.java
@AutoConfiguration(
afterName = "org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration") // 晚于 Jackson 配置
@ConditionalOnClass(GreetingClient.class) // classpath 有 SDK 才生效(元注解中不可用 Class 字面量,这里直接用没问题)
@EnableConfigurationProperties(GreetingProperties.class)
public class GreetingAutoConfiguration {
@Bean
@ConditionalOnMissingBean // 用户自己定义了 GreetingService 就退让
public GreetingService greetingService(GreetingClient client) {
return new GreetingService(client);
}
}
② 声明候选(放在 starter 模块的 resources 下):
properties
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.greeting.autoconfigure.GreetingAutoConfiguration
③ 编译期元数据 :starter 的 build 里引入 spring-boot-autoconfigure-processor 作为 annotation processor(它扫描 @ConditionalOnClass 等注解,生成 spring-autoconfigure-metadata.properties)。这样你的自动配置在过滤阶段就能被元数据加速,而不是等运行时反射。
10.3 断点走查
给 SpringBootCondition.matches() 的 logOutcome 前一行、AutoConfigurationImportSelector.getAutoConfigurationEntry() 第一行、AutoConfigurationSorter.getInPriorityOrder() 第一行各打一个断点,启动应用,观察调用栈:
scss
① getAutoConfigurationEntry()
├─ getCandidateConfigurations() → ImportCandidates.load(...) 扫描到你的 imports 文件
└─ getConfigurationClassFilter().filter()
└─ OnClassCondition.getOutcomes() ← 元数据命中,Class.forName("com.example.sdk.GreetingClient") 存在 → 放行
② ConfigurationClassParser 把 GreetingAutoConfiguration 当作 @Configuration 解析
└─ ConditionEvaluator.shouldSkip(metadata, PARSE_CONFIGURATION)
└─ OnClassCondition.getMatchOutcome() ← 匹配 → 进入候选
③ ConfigurationClassBeanDefinitionReader 注册 @Bean 方法
└─ ConditionEvaluator.shouldSkip(metadata, REGISTER_BEAN)
└─ OnBeanCondition.getMatchOutcome() ← @ConditionalOnMissingBean 检查:
beanFactory.getBeanNamesForType(GreetingService.class) 为空 → 注册成功
如果你在应用里再写一个 GreetingService 的 @Bean,第三步会看到 getMatchingBeans 命中了你定义的 Bean → 自动配置退让。整个退让机制就是这么实现的。
启动后加 --debug,你会看到自己的自动配置出现在报告里:
kotlin
GreetingAutoConfiguration matched:
- @ConditionalOnClass found required class
'com.example.sdk.GreetingClient' (OnClassCondition)
10.4 排除与调试
- 排除自动配置 :
@SpringBootApplication(exclude = GreetingAutoConfiguration.class)、excludeName = "...",或属性spring.autoconfigure.exclude=com.example.greeting.autoconfigure.GreetingAutoConfiguration------前两者走注解属性,后者走getExclusions()里的 Environment 读取,最终都在第 6 步removeAll生效; - 查看报告 :
--debug/ 日志级别 DEBUG 看CONDITIONS EVALUATION REPORT;生产用 Actuatorconditions端点; - AOT 构建排错 :
./gradlew bootProcessAot时关注构建日志里ConditionEvaluationReportLoggingProcessor输出的报告------构建期没匹配上的自动配置,运行期同样不会生效,但原因在构建期就能看到。
十一、总结
一张图回顾全文:
scss
META-INF/spring/...AutoConfiguration.imports(99 个文件,每行一个类)
│ ImportCandidates 合并(classLoader.getResources)
▼
AutoConfigurationImportSelector.getAutoConfigurationEntry()
├─ 去重 / 排除(注解 + spring.autoconfigure.exclude + replacements 替换)
├─ ① 过滤:AutoConfigurationImportFilter(编译期元数据 + Class.forName,不加载字节码)
└─ ② 排序:AutoConfigurationSorter(字母序 → @AutoConfigureOrder → 拓扑排序 + 环检测)
▼
作为 @Configuration 进入标准解析
└─ ③ 条件评估:ConditionEvaluator.shouldSkip()
├─ PARSE_CONFIGURATION:OnClassCondition / OnWebApplicationCondition(ASM 元数据 + 探针类)
└─ REGISTER_BEAN:OnBeanCondition(@ConditionalOnBean / @ConditionalOnMissingBean 查 BeanDefinition)
▼
结果写入 ConditionEvaluationReport → --debug 报告 / Actuator conditions
三个核心设计思想:
- 延迟导入(DeferredImportSelector)------自动配置永远晚于用户配置,这是"退让"机制的根基;
- 两阶段评估(提前过滤 + 完整评估)------能用类名存在性解决的事,绝不去读字节码;能用编译期元数据解决的事,绝不放运行时;
- 构建期前置(AOT)------4.x 把 AOT 基础设施下沉到 Spring Framework 7,自动配置的条件评估可以在构建期完整执行一遍并固化为代码,启动期免扫描、免动态条件评估。