Spring Boot 4.1.1 深度源码解析-第 14 章 · 自动配置引擎:AutoConfigurationImportSelector 全流程
前情提要:第 13 章把"原始 Bean 定义从哪来"讲完了。我们留下一个空洞——
@EnableAutoConfiguration是怎么变成 100+ 个自动配置类的?本章把它填满。
0. 本章要回答的六个问题
@SpringBootApplication
public class App { ... }
这一行注解,最终让容器里多出了几十个(引入 starter 后可达上百个)自动配置类。中间发生了什么?
| # | 问题 | 关键答案(先剧透) |
|---|---|---|
| 1 | 自动配置类的"名单"从哪读? | META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,由 ImportCandidates 读取 |
| 2 | 几百个候选里,绝大多数会被丢弃,谁先筛? | AutoConfigurationImportFilter(3 个),在读字节码之前就筛掉 |
| 3 | 筛选的依据是什么?不该加载类吗? | AutoConfigurationMetadata(编译期生成的 properties),避免加载候选类 |
| 4 | 为什么自动配置必须"延后"? | DeferredImportSelector —— 否则 @ConditionalOnMissingBean 永远为真 |
| 5 | 剩下的候选怎么排序? | AutoConfigurationSorter 三轮:字母序 → @AutoConfigureOrder → @AutoConfigureBefore/After DFS |
| 6 | 排序结果怎么变成 Bean 定义? | AutoConfigurationGroup.selectImports() 返回 Entry,交给 ConfigurationClassParser 递归解析 |
读完本章,读者应该能回答:
"为什么我加了一个
@Bean DataSource,DataSourceAutoConfiguration就不生效了?" "为什么spring.autoconfigure.exclude写错类名会启动失败,而写对了却什么都没排除?" "为什么@AutoConfigureAfter有时会失效?"
1. 全景:@EnableAutoConfiguration 的三件事
先看注解本体(本仓库 EnableAutoConfiguration.java:78-81):
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
它只做了三件事:
@EnableAutoConfiguration
│
├─ ① @AutoConfigurationPackage
│ └─ @Import(AutoConfigurationPackages.Registrar)
│ → 记录主类所在包(第 13 章 §14)
│
├─ ② @Import(AutoConfigurationImportSelector.class)
│ └─ 本章全部内容
│
└─ ③ 声明 exclude / excludeName 两个属性
└─ 供 AutoConfigurationImportSelector 读取
注意 ③:属性本身不带任何逻辑。它们是"数据",由 AutoConfigurationImportSelector 用 AnnotationMetadata 读出来。这是 Spring 注解体系的一贯风格——注解是元数据载体,逻辑在处理器里。
1.1 ENABLED_OVERRIDE_PROPERTY
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
这个常量的名字里有个 spring.boot 前缀——注意不是 spring.autoconfigure。这是一个历史遗留的命名不一致(Boot 1.x 就这么叫了)。
用法:
spring:
boot:
enableautoconfiguration: false
但它在 2.1 被弃用后又被"半复活"了——现在的语义见 §3.2。
1.2 与 @SpringBootApplication 的关系
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { ... })
public @interface SpringBootApplication { ... }
@SpringBootApplication 元标注了 @EnableAutoConfiguration。所以:
AutoConfigurationImportSelector的getAttributes()读的是EnableAutoConfiguration的属性(不是SpringBootApplication的)- 而
@SpringBootApplication用@AliasFor(annotation = EnableAutoConfiguration.class)把exclude/excludeName别名过去(第 13 章讲过)
所以这两种写法等价:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@SpringBootApplication(excludeName = "org.springframework.boot...DataSourceAutoConfiguration")
2. ImportCandidates:名单从哪来
2.1 位置常量
// ImportCandidates.java: 42
private static final String LOCATION = "META-INF/spring/%s.imports";
%s 是注解的全限定名。对于自动配置:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对应代码(AutoConfigurationImportSelector.java:202):
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, getBeanClassLoader());
其中 this.autoConfigurationAnnotation 默认是 AutoConfiguration.class(构造行 113–116):
AutoConfigurationImportSelector(@Nullable Class<?> autoConfigurationAnnotation) {
this.autoConfigurationAnnotation = (autoConfigurationAnnotation != null) ? autoConfigurationAnnotation
: AutoConfiguration.class;
}
这个字段的存在是为了子类化——ImportAutoConfigurationImportSelector 就传了别的值(§13)。
2.2 本仓库的实际规模
$ find . -name "*.imports" -not -path "*/build/*" | wc -l
178
178 个 .imports 文件,分散在各模块的 src/main/resources/META-INF/spring/ 下。
核心模块 spring-boot-autoconfigure 自己的那一份只有 12 行:
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
注意:这 12 个是"核心中的核心"(不依赖任何可选依赖)。绝大部分自动配置(DataSource、Web、JPA、Redis……)都在各自的模块里有独立的 .imports 文件。
这就是第 04 章讲的"模块化自动配置":spring-boot-jdbc 的 .imports 只在 spring-boot-jdbc jar 里,你不引入这个 jar,那份名单就不存在 → 自动配置天然按需生效。
对比 3.x 的 spring.factories:那是一个中心化的大文件,所有自动配置都在 spring-boot-autoconfigure 的 spring.factories 里,靠 @ConditionalOnClass 判活。4.x 的 .imports 是去中心化的——每个模块自带名单。
这个改动的两个收益:
- 候选数量大大减少(不引入 jdbc 就没有 jdbc 的候选),启动更快
- 模块自描述,不需要改中心文件
2.3 读取逻辑
public static ImportCandidates load(Class<?> annotation, @Nullable ClassLoader classLoader) {
Assert.notNull(annotation, "'annotation' must not be null");
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format(LOCATION, annotation.getName());
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}
return new ImportCandidates(importCandidates);
}
private static List<String> readCandidateConfigurations(URL url) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(new UrlResource(url).getInputStream(), StandardCharsets.UTF_8))) {
List<String> candidates = new ArrayList<>();
String line;
while ((line = reader.readLine()) != null) {
line = stripComment(line);
line = line.trim();
if (line.isEmpty()) {
continue;
}
candidates.add(line);
}
return candidates;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load configurations from location [" + url + "]", ex);
}
}
private static String stripComment(String line) {
int commentStart = line.indexOf(COMMENT_START); // "#"
if (commentStart == -1) {
return line;
}
return line.substring(0, commentStart);
}
逐条规则:
- 编码写死 UTF-8(不依赖平台默认编码)✅
- 注释用
#,但注意它是indexOf("#")—— 行内注释也支持:会被截断成com.example.Foo # 这是一条注释com.example.Foo。这一点与.properties的#规则不同(后者只在行首)。 trim()后跳过空行- 不做任何去重 —— 重复的类名会留在列表里,交给
removeDuplicates()处理(§4.3) - 不做任何排序 —— 顺序就是 classpath 枚举顺序 × 文件内行顺序
2.4 【重要】候选顺序是"不确定的"
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}
classLoader.getResources(location) 的枚举顺序取决于:
- 目录形态(开发时
build/resources/main):文件系统遍历顺序 - jar 形态(生产):jar 内条目顺序 × classpath 上 jar 的顺序
- LaunchedClassLoader(Boot 可执行 jar,第 26 章):又有自己的顺序
这意味着:候选列表的原始顺序是不确定的、不可依赖的。
这正是 AutoConfigurationSorter 存在的根本原因(§9)——排序器必须先把顺序"归一化"(字母序),再叠加语义顺序,才能让最终顺序稳定可预测。
这是本系列反复出现的模式:当输入顺序不可控时,Boot 会先做一次"规范化排序"再叠加语义。第 07 章的 contributor 树、第 10 章的优先级推导都见过类似思路。
2.5 空名单的硬断言
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.");
触发场景:
- 用了自定义打包(shade / 自研 Loader),把
META-INF/spring/*.imports丢了 - 用了某些老版本的构建插件,资源过滤把
.imports排除了 - 手写了一个
@EnableAutoConfiguration但 classpath 上一份.imports都没有
错误信息里那句 "If you are using a custom packaging, make sure that file is correct." 是 Boot 团队在无数 issue 后加上的——它精准指向了最常见的根因。这是一个值得学习的错误信息写法。
2.6 与 spring.factories 的分工(回顾第 04 章)
Boot 4.x 里,自动配置名单走 .imports,但另外三张表仍走 spring.factories(第 11 章 §2 讲过):
| 表 | 位置 | 本章相关 |
|---|---|---|
AutoConfiguration | META-INF/spring/*.imports | ✅ 本章主角 |
AutoConfigurationImportFilter | spring.factories | ✅ §6 |
AutoConfigurationImportListener | spring.factories | ✅ §8.4 |
PropertySourceLoader 等 | spring.factories | 第 11 章 |
为什么 filter 和 listener 还在 spring.factories?
这是个值得思考的问题。我能给出的解释(标注为推演):.imports 机制是为"大量同构候选"设计的(每个模块贡献一批),而 filter / listener 是少量、全局、与模块无关的扩展点,用 spring.factories 更自然。另外 spring.factories 支持 SpringFactoriesLoader.loadFactories() 的实例化 + 排序(@Order),而 ImportCandidates 只返回类名字符串——filter 需要实例化,所以用 loadFactories 更方便。
3. AutoConfigurationImportSelector:类结构与两个入口
3.1 类声明
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
static final int ORDER = Ordered.LOWEST_PRECEDENCE - 1;
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
private static final String[] NO_IMPORTS = {};
private static final String PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE = "spring.autoconfigure.exclude";
...
}
继承/实现的六种能力:
| 接口 | 用途 |
|---|---|
DeferredImportSelector | 核心。延后到所有配置类解析完再执行 |
BeanClassLoaderAware | 读 .imports、加载候选类需要 |
ResourceLoaderAware | 传给 filter / listener |
BeanFactoryAware | 断言必须是 ConfigurableListableBeanFactory |
EnvironmentAware | 读 spring.autoconfigure.exclude、spring.boot.enableautoconfiguration |
Ordered | LOWEST_PRECEDENCE - 1 |
ORDER = LOWEST_PRECEDENCE - 1 —— 倒数第二低。含义:当有多个 DeferredImportSelector 时,自动配置的优先级最低(最后执行),保证用户的 @Import 先处理。但又不至于是最低(LOWEST_PRECEDENCE),留了一格给"比自动配置还晚"的扩展。
3.2 isEnabled:那个"半复活"的开关
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
}
return true;
}
三个细节:
- 默认
true—— 不配置就是启用 getClass() == AutoConfigurationImportSelector.class守卫 —— 只有"正好是这个类的实例"才读开关;子类永远返回true。- 为什么?子类(
ImportAutoConfigurationImportSelector)有自己的启用语义,spring.boot.enableautoconfiguration不该影响测试切片。 - 这与第 13 章
TypeExcludeFilter的getClass() == TypeExcludeFilter.class是同一个模式——用精确类比较来区分"是不是我自己"。
- 为什么?子类(
- 属性名是
spring.boot.enableautoconfiguration(不是spring.autoconfigure.*),见 §1.1
selectImports 与 getAutoConfigurationEntry 都检查它:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
双重检查是因为 selectImports(非 Group 路径)和 getAutoConfigurationEntry(Group 路径)是两个独立入口。Group 路径下 selectImports 不会被调用(Framework 的 DeferredImportSelectorHandler 会走 Group 分支),但保留它是为了兼容直接调用。
3.3 getExclusionFilter()
@Override
public Predicate<String> getExclusionFilter() {
return this::shouldExclude;
}
private boolean shouldExclude(String configurationClassName) {
return getConfigurationClassFilter().filter(Collections.singletonList(configurationClassName)).isEmpty();
}
DeferredImportSelector.getExclusionFilter() 是 Spring Framework 5.x 加的接口方法,语义是:
"对于由这个 selector 导入的类,返回 true 表示应当从后续处理中排除。"
Boot 的实现:用同一套 ConfigurationClassFilter 去筛。也就是说:
- 如果一个自动配置类在
match()阶段被OnClassCondition判为不匹配 → 它也在getExclusionFilter里被排除 - 这是一个一致性保证:导入名单和后续排除判定用同一把尺子
重要:这个 filter 只在"类是通过本 selector 导入的"这一前提下生效。它不会影响用户自己的 @Import。
3.4 四个 Aware 的 setter 与 invokeAwareMethods
private void invokeAwareMethods(Object instance) {
if (instance instanceof Aware) {
if (instance instanceof BeanClassLoaderAware beanClassLoaderAwareInstance) {
beanClassLoaderAwareInstance.setBeanClassLoader(this.beanClassLoader);
}
if (instance instanceof BeanFactoryAware beanFactoryAwareInstance) {
beanFactoryAwareInstance.setBeanFactory(this.beanFactory);
}
if (instance instanceof EnvironmentAware environmentAwareInstance) {
environmentAwareInstance.setEnvironment(this.environment);
}
if (instance instanceof ResourceLoaderAware resourceLoaderAwareInstance) {
resourceLoaderAwareInstance.setResourceLoader(this.resourceLoader);
}
}
}
为什么手写而不是让容器注入?
因为 filter / listener 是通过 SpringFactoriesLoader.loadFactories() 反射创建的,不是 Spring Bean——容器不会给它们注入 Aware。所以 AutoConfigurationImportSelector 必须自己转发。
注意 setBeanFactory 传的是自己的 this.beanFactory(ConfigurableListableBeanFactory):
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(ConfigurableListableBeanFactory.class, beanFactory); // ← 硬断言
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
为什么要断言成 ConfigurableListableBeanFactory? 因为 ConditionEvaluationReport 需要 ConfigurableListableBeanFactory(它往里面放一个单例),OnBeanCondition 需要 getBeanNamesForType 等 Listable 能力。
4. getAutoConfigurationEntry:七步流水线
这是全章的中枢(AutoConfigurationImportSelector.java:142-155):
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 行,我这里按语义拆成 9 步。)
4.1 ② getAttributes
protected @Nullable AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
String name = getAnnotationClass().getName();
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
Assert.state(attributes != null, () -> "No auto-configuration attributes found. Is " + metadata.getClassName()
+ " annotated with " + ClassUtils.getShortName(name) + "?");
return attributes;
}
protected Class<?> getAnnotationClass() {
return EnableAutoConfiguration.class;
}
getAnnotationAttributes(name, true)的true= classValuesAsString,即把Class<?>值转成String。所以exclude = {Foo.class}读出来是"com.example.Foo"。- 这个转换很重要:它避免了加载
Foo类(ASM 只读字节码里的类名字符串)
- 这个转换很重要:它避免了加载
- 断言信息很友好:
Is com.example.App annotated with EnableAutoConfiguration? getAnnotationClass()可被子类覆盖——ImportAutoConfigurationImportSelector返回null并覆盖整个方法
4.2 ③ getCandidateConfigurations
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
@Nullable AnnotationAttributes attributes) {
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation,
getBeanClassLoader());
List<String> configurations = importCandidates.getCandidates();
Assert.state(!CollectionUtils.isEmpty(configurations), ...);
return configurations;
}
(已在 §2 详述。)
4.3 ④ removeDuplicates
protected final <T> List<T> removeDuplicates(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
LinkedHashSet 保序去重 —— 保留首次出现的位置。
为什么会有重复?
- 同一个
.imports文件被两个 jar 提供(比如 classpath 上有两个版本的 starter) - 同一行被写两次
- 多个
.imports文件(来自不同 jar)列了同一个类
为什么用 LinkedHashSet 而不是 HashSet? 因为后面还要排序,但排序前的"字母序归一化"在 AutoConfigurationSorter 里做;这里保序是为了可复现性——虽然原始顺序不确定,但至少"去重保留首次"是确定性的操作。
4.4 ⑤ getExclusions:三个来源
protected Set<String> getExclusions(AnnotationMetadata metadata, @Nullable AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
if (attributes != null) {
excluded.addAll(asList(attributes, "exclude")); // ① 注解的 exclude
excluded.addAll(asList(attributes, "excludeName")); // ② 注解的 excludeName
}
excluded.addAll(getExcludeAutoConfigurationsProperty()); // ③ spring.autoconfigure.exclude
return getAutoConfigurationReplacements().replaceAll(excluded); // ④ 替换映射
}
三个来源 + 一次替换:
| 来源 | 形式 | 适用场景 |
|---|---|---|
exclude | Class<?>[] | 编译期可引用该自动配置类 |
excludeName | String[] | 类不在 classpath 上(如只想在某些环境排除) |
spring.autoconfigure.exclude | 属性 | 运行时决定(不同 profile 排不同配置) |
exclude vs excludeName 的选择:@SpringBootApplication 的 Javadoc 明确说了:
In order to use this annotation as a meta-annotation, only use the
excludeNameattribute.
原因:当 @SpringBootApplication 被用作元注解(比如你项目里定义 @MyApp 组合注解),exclude = Foo.class 会让 ASM 需要解析 Foo 的字节码;如果 Foo 不在 classpath,会失败。而 excludeName = "com.example.Foo" 只是一个字符串,永远安全。
spring.autoconfigure.exclude 的绑定
protected List<String> getExcludeAutoConfigurationsProperty() {
Environment environment = getEnvironment();
if (environment == null) {
return Collections.emptyList();
}
if (environment instanceof ConfigurableEnvironment) {
Binder binder = Binder.get(environment);
return binder.bind(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class)
.map(Arrays::asList)
.orElse(Collections.emptyList());
}
String[] excludes = environment.getProperty(PROPERTY_NAME_AUTOCONFIGURE_EXCLUDE, String[].class);
return (excludes != null) ? Arrays.asList(excludes) : Collections.emptyList();
}
两条路径:
- 优先
Binder(ConfigurableEnvironment)→ 支持逗号分隔 + 索引形式(第 08 章讲过String[]的绑定) - 回退
environment.getProperty(..., String[].class)→ 只支持逗号分隔
元数据里声明的类型与实际不符:
{
"name": "spring.autoconfigure.exclude",
"type": "java.util.List<java.lang.Class>",
"description": "Auto-configuration classes to exclude."
}
元数据写的是 List<Class>,但代码绑的是 String[]。这是一个元数据与实现的小不一致——元数据只影响 IDE 提示,不影响运行时。实践中写 YAML 列表或逗号串都可以。
④ 替换映射
return getAutoConfigurationReplacements().replaceAll(excluded);
见 §11。
4.5 ⑥ checkExcludedClasses:防呆检查
private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
List<String> invalidExcludes = new ArrayList<>(exclusions.size());
ClassLoader classLoader = (this.beanClassLoader != null) ? this.beanClassLoader : getClass().getClassLoader();
for (String exclusion : exclusions) {
if (ClassUtils.isPresent(exclusion, classLoader) && !configurations.contains(exclusion)) {
invalidExcludes.add(exclusion);
}
}
if (!invalidExcludes.isEmpty()) {
handleInvalidExcludes(invalidExcludes);
}
}
protected void handleInvalidExcludes(List<String> invalidExcludes) {
StringBuilder message = new StringBuilder();
for (String exclude : invalidExcludes) {
message.append("\t- ").append(exclude).append(String.format("%n"));
}
throw new IllegalStateException(String.format(
"The following classes could not be excluded because they are not auto-configuration classes:%n%s",
message));
}
判定"无效排除"的条件是两个 AND:
ClassUtils.isPresent(exclusion, classLoader)—— 这个类在 classpath 上!configurations.contains(exclusion)—— 但它不在自动配置候选名单里
为什么两个都要?
- 条件 1 排除"类不在 classpath"的情况:如果你用
excludeName排除了一个不存在的类,不报错(这是excludeName的设计用途) - 条件 2 才是真正的判定
这个检查的价值:防止"写错了类名但静默无效"。经典场景:
@SpringBootApplication(exclude = {DataSourceAutoConfig.class}) // ← 少了个 uration
如果没有这个检查,你以为排除了,实际没有,然后花三小时查为什么数据源还是被创建了。
一个反直觉的边界:
@SpringBootApplication(exclude = {SomeClass.class}) // SomeClass 是一个自动配置类,但被 spring.autoconfigure.exclude 之外的方式排除了
没问题,因为检查的是完整候选名单(configurations 是 candidates,还没被 filter 筛)。
但另一个边界会出问题:
如果你排除的类所在的 jar 引入后,它的 .imports 又因为某种原因没被读到(比如候选为空触发了 §2.5 的断言),那么……实际上会先触发断言,不会到这里。
4.6 ⑦ removeAll 与 ⑧ filter
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
顺序很重要:先排除用户显式指定的,再用条件 filter 筛。
为什么不能反过来? 如果先 filter,checkExcludedClasses 看到的 configurations 就是"筛选后"的名单,那么"排除一个被 OnClassCondition 判死的类"就会被误判为"无效排除"→ 报错。先排除再 filter 就没这个问题。
4.7 ⑨ fireAutoConfigurationImportEvents
private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
if (!listeners.isEmpty()) {
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
for (AutoConfigurationImportListener listener : listeners) {
invokeAwareMethods(listener);
listener.onAutoConfigurationImportEvent(event);
}
}
}
protected List<AutoConfigurationImportListener> getAutoConfigurationImportListeners() {
return SpringFactoriesLoader.loadFactories(AutoConfigurationImportListener.class, this.beanClassLoader);
}
先 if (!listeners.isEmpty()) 再构造 event —— 避免无谓的对象创建。
本仓库只有一个 listener(spring.factories:11-12):
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
它的实现(本仓库):
class ConditionEvaluationReportAutoConfigurationImportListener
implements AutoConfigurationImportListener, BeanFactoryAware {
private @Nullable ConfigurableListableBeanFactory beanFactory;
@Override
public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) {
if (this.beanFactory != null) {
ConditionEvaluationReport report = ConditionEvaluationReport.get(this.beanFactory);
report.recordEvaluationCandidates(event.getCandidateConfigurations());
report.recordExclusions(event.getExclusions());
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (beanFactory instanceof ConfigurableListableBeanFactory listableBeanFactory)
? listableBeanFactory : null;
}
}
作用:把"候选名单"和"排除名单"记进 ConditionEvaluationReport。这样 --debug 或 /actuator/conditions 才能告诉你:
"这个类曾经是候选,但被条件判死了" vs "这个类从来不是候选(jar 没引入)"
这两种情况的排查方向完全不同——前者查条件,后者查依赖。这个区分是自动化诊断的关键。
4.8 AutoConfigurationEntry 的结构
protected static class AutoConfigurationEntry {
private final List<String> configurations;
private final Set<String> exclusions;
private AutoConfigurationEntry() {
this.configurations = Collections.emptyList();
this.exclusions = Collections.emptySet();
}
AutoConfigurationEntry(Collection<String> configurations, Collection<String> exclusions) {
this.configurations = new ArrayList<>(configurations);
this.exclusions = new HashSet<>(exclusions);
}
...
}
注意两个构造器的可见性:
- 私有无参构造 →
EMPTY_ENTRY(不可变空对象,复用单例) - 包级/protected 两参构造 → 正常路径
configurations 是 List(有序),exclusions 是 Set(无序) —— 因为顺序对配置重要,对排除不重要。
EMPTY_ENTRY 与 NO_IMPORTS 两个常量分别用于 Group 路径和 selectImports 路径(§3.2)。
5. ConfigurationClassFilter:字节码之前的预筛
5.1 三个 filter(来自 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
注意声明顺序:OnBeanCondition → OnClassCondition → OnWebApplicationCondition。
但实际执行顺序不一定是这个(SpringFactoriesLoader.loadFactories 会按 @Order 排序)。OnClassCondition 上有 @Order(Ordered.HIGHEST_PRECEDENCE)(本仓库 OnClassCondition.java:45),所以它排在最前——这有性能意义:先做最便宜、最能筛掉大批候选的检查。
5.2 ConfigurationClassFilter 的实现
private static class ConfigurationClassFilter {
private final AutoConfigurationMetadata autoConfigurationMetadata;
private final List<AutoConfigurationImportFilter> filters;
ConfigurationClassFilter(ClassLoader classLoader, List<AutoConfigurationImportFilter> filters) {
this.autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(classLoader);
this.filters = filters;
}
List<String> filter(List<String> configurations) {
long startTime = System.nanoTime();
@Nullable String[] candidates = StringUtils.toStringArray(configurations);
boolean skipped = false;
for (AutoConfigurationImportFilter filter : this.filters) {
boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
for (int i = 0; i < match.length; i++) {
if (!match[i]) {
candidates[i] = null; // ← 置 null,不是移除
skipped = true;
}
}
}
if (!skipped) {
return configurations; // ← 一个都没筛掉,直接返回原列表
}
List<String> result = new ArrayList<>(candidates.length);
for (String candidate : candidates) {
if (candidate != null) {
result.add(candidate);
}
}
if (logger.isTraceEnabled()) {
int numberFiltered = configurations.size() - result.size();
logger.trace("Filtered " + numberFiltered + " auto configuration class in "
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
}
return result;
}
}
三个精巧之处:
① 数组置 null 而不是 List.remove
如果用 List.remove,每次移除都要 System.arraycopy,且会改变后续元素的索引——多个 filter 串行处理时索引会错乱。用"定长数组 + 置 null",每个 filter 看到的索引始终一致。
这是接口注释要求的:
/**
* @param autoConfigurationClasses ... This array may contain {@code null} elements.
* Implementations should not change the values in this array.
*/
即:实现必须能容忍 null 元素,且不能修改传入数组。
② skipped 短路
如果一个都没筛掉,直接返回原 configurations(同一个对象,不是拷贝)。避免了无谓的 ArrayList 创建。
在"引入很多 starter"的应用里,通常都会筛掉一些;但在最小应用里这一步是零开销。
③ logger.isTraceEnabled() 的守卫
numberFiltered 的计算和 toMillis 只在 trace 开启时做。注意 startTime = System.nanoTime() 是无条件取的——nanoTime() 很便宜(约 20-50ns),无条件取可以接受。
5.3 懒加载 + volatile
private volatile @Nullable ConfigurationClassFilter configurationClassFilter;
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;
}
注意这不是严格的双重检查锁(没有 synchronized)——如果并发调用,可能创建多个 ConfigurationClassFilter 实例。后果只是多读一次 spring-autoconfigure-metadata.properties,无害。
**这是"benign race"(良性竞态)**的教科书例子:用 volatile 保证可见性,容忍重复创建,换取无锁性能。
(同样的模式也用在 AutoConfigurationReplacements 上,行 295-303。)
5.4 FilteringSpringBootCondition:一个 filter 的两副面孔
abstract class FilteringSpringBootCondition extends SpringBootCondition
implements AutoConfigurationImportFilter, BeanFactoryAware, BeanClassLoaderAware {
@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()); // ← null 视为 match
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;
}
...
}
关键:这同一个类同时是:
SpringBootCondition(在ConfigurationClassParser解析每个自动配置类时求值@ConditionalOnClass)AutoConfigurationImportFilter(在导入阶段批量预筛)
为什么要两套?
因为性能。正式的条件求值需要:
- 加载自动配置类的字节码(ASM 或反射)
- 解析注解属性
- 构造
ConditionContext - 求值
而预筛只需要读编译期生成的 properties,不需要碰字节码。在几百个候选的场景下,这个差异是数量级的。
注意 outcome == null 视为 match —— 即"元数据里没有这个类的记录"时不筛掉它,交给后续正式求值决定。这是一个保守默认:宁可多留,不可误杀。
5.5 OnClassCondition 的并行优化
@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();
}
}
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();
@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;
}
这段代码的注释极其珍贵,它记录了 Boot 团队的性能实验结论:
Using a single additional thread seems to offer the best performance. More threads make things worse.
为什么只开一个额外线程?
因为 @ConditionalOnClass 的检查本质是大量极小的 Class.forName 调用(微秒级)。开更多线程的同步/调度开销超过了并行收益。这与"并行越多越快"的直觉相反——这是实测得出的结论,不是理论推导。
注意执行顺序的巧妙:
@Nullable ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes(); // 先同步算后半
@Nullable ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes(); // 再取前半(此时后台已完成)
firstHalfResolver 是 ThreadedOutcomesResolver,构造时就启动后台线程开始算前半段。然后主线程同步算后半段。最后 firstHalfResolver.resolveOutcomes() 阻塞等待后台结果(通常已完成)。
这是一个标准的 fork-join 手写实现,把"主线程也利用起来",而不是让主线程干等。
触发条件:length > 1 && availableProcessors() > 1。单核或只有一个候选时不走并行。
6. AutoConfigurationMetadata:避免读字节码的关键
6.1 它是什么
final class AutoConfigurationMetadataLoader {
private static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
return loadMetadata(classLoader, PATH);
}
static AutoConfigurationMetadata loadMetadata(@Nullable ClassLoader classLoader, String path) {
try {
Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
: ClassLoader.getSystemResources(path);
Properties properties = new Properties();
while (urls.hasMoreElements()) {
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
}
return loadMetadata(properties);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
}
}
...
}
文件格式(编译期由 spring-boot-autoconfigure-processor 生成):
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration=
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.ConditionalOnClass=javax.sql.DataSource,org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.AutoConfigureBefore=org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration
org.springframework.boot.autoconfigure.condition.OnBeanCondition=...
三种键:
{类名}—— 存在即表示"这个类被处理过"(wasProcessed){类名}.ConditionalOnClass—— 逗号分隔的类名列表{类名}.AutoConfigureBefore/.AutoConfigureAfter/.AutoConfigureOrder
6.2 wasProcessed 的两条路径
@Override
public boolean wasProcessed(String className) {
return this.properties.containsKey(className);
}
在 AutoConfigurationSorter.AutoConfigurationClass 里:
private boolean wasProcessed() {
return (this.autoConfigurationMetadata != null
&& this.autoConfigurationMetadata.wasProcessed(this.className));
}
private Set<String> getClassNames(String metadataKey, Class<? extends Annotation> annotation) {
Set<String> annotationValue = wasProcessed() ? getSet(metadataKey) : getAnnotationValue(annotation);
return applyReplacements(annotationValue);
}
两条路径:
wasProcessed() == true | wasProcessed() == false | |
|---|---|---|
| 数据来源 | AutoConfigurationMetadata(properties) | ASM 读字节码(getAnnotationMetadata()) |
| 性能 | 极快(内存查表) | 慢(I/O + 解析) |
| 适用场景 | 编译期处理过的(正常构建) | 没跑 annotation processor 的场景(如手工编译、某些 IDE) |
这是一个优雅的降级设计:有元数据用元数据,没有就退回读字节码。功能永远正确,只是慢一点。
6.3 为什么需要它
回到核心问题:Boot 如何知道 DataSourceAutoConfiguration 需要 javax.sql.DataSource,而不用加载 DataSourceAutoConfiguration 这个类?
答案就是 spring-autoconfigure-metadata.properties。这个 properties 是编译期由 annotation processor 扫描 @ConditionalOnClass 等注解生成的。
收益:
- 避免加载几百个候选类的字节码
- 避免这些类触发静态初始化(很多自动配置类引用了可选依赖,加载它们本身就可能失败)
- 避免
NoClassDefFoundError:如果DataSourceAutoConfiguration的某个方法签名引用了不存在的类,加载它会炸;读元数据完全不会
这条是"预筛"能成立的基石。
6.4 PropertiesAutoConfigurationMetadata 的类型转换
@Override
public @Nullable Integer getInteger(String className, String key, @Nullable Integer defaultValue) {
String value = get(className, key);
return (value != null) ? Integer.valueOf(value) : defaultValue;
}
@Override
public @Nullable Set<String> getSet(String className, String key, @Nullable Set<String> defaultValue) {
String value = get(className, key);
return (value != null) ? StringUtils.commaDelimitedListToSet(value) : defaultValue;
}
Set 用逗号分隔(StringUtils.commaDelimitedListToSet)。所以元数据里 ConditionalOnClass=a,b,c 会变成三个元素。
注意:commaDelimitedListToSet 不会 trim 每个元素(实际上它会 trim?Spring 的实现会 trim——这里标注为未在本仓库核实,StringUtils 是 Framework 类)。实践上元数据生成时不会加空格,所以无影响。
7. AutoConfigurationGroup:为什么需要 Group
7.1 DeferredImportSelector.Group 的意义
@Override
public Class<? extends Group> getImportGroup() {
return AutoConfigurationGroup.class;
}
Spring Framework 的 DeferredImportSelector 可以返回一个 Group 实现类。语义是:
同一个 Group 的所有
DeferredImportSelector的process()会先被全部调用,最后统一调用一次selectImports()。
为什么自动配置需要这个?
因为可能有多个 @EnableAutoConfiguration:
@SpringBootApplication // ← 第一个
public class App { }
@Configuration
@EnableAutoConfiguration // ← 第二个(比如从某个库引入)
class SomeLibConfig { }
或者测试里:
@SpringBootTest
@ImportAutoConfiguration(...) // ← 另一个 selector
没有 Group,每个 selector 独立 selectImports(),会产生多个独立的导入批次——各自去重、各自排序,最终顺序混乱,且 @AutoConfigureAfter 的跨批次依赖失效。
有 Group,所有 selector 的贡献被聚合后统一处理一次 → 全局去重、全局排序。
7.2 process():聚合并校验
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationImportSelector autoConfigurationImportSelector = (AutoConfigurationImportSelector) deferredImportSelector;
AutoConfigurationReplacements autoConfigurationReplacements = autoConfigurationImportSelector
.getAutoConfigurationReplacements();
Assert.state(
this.autoConfigurationReplacements == null
|| this.autoConfigurationReplacements.equals(autoConfigurationReplacements),
"Auto-configuration replacements must be the same for each call to process");
this.autoConfigurationReplacements = autoConfigurationReplacements;
AutoConfigurationEntry autoConfigurationEntry = autoConfigurationImportSelector
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
四个要点:
-
硬断言只支持
AutoConfigurationImportSelector—— 因为 Group 需要访问它的 protected 方法(getAutoConfigurationEntry、getAutoConfigurationReplacements)。其他类型的 selector 无法参与这个 Group。 -
AutoConfigurationReplacements必须一致 —— 如果两次process()拿到的替换表不同(比如 classpath 上有冲突的.replacements文件),直接报错。这是一个一致性校验,避免排序时出现不确定的替换行为。AutoConfigurationReplacements实现了equals/hashCode(AutoConfigurationReplacements.java),正是为了这个断言。 -
autoConfigurationEntries累积所有 entry —— 每个process()一次调用贡献一个。 -
entries是一个Map<String, AnnotationMetadata>—— 记录"哪个类是由哪个配置类导入的"。用putIfAbsent(首次胜出):如果一个类被多个配置类导入,记录第一个。这个 Map 的作用在
selectImports()里:构造Entry(metadata, importClassName)需要AnnotationMetadata(Framework 的DeferredImportSelector.Group.Entry类型是(AnnotationMetadata, String),记录了"导入来源")。
7.3 selectImports():三步产出
@Override
public Iterable<Entry> selectImports() {
if (this.autoConfigurationEntries.isEmpty()) {
return Collections.emptyList();
}
Set<String> allExclusions = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getExclusions)
.flatMap(Collection::stream)
.collect(Collectors.toSet()); // ① 合并所有排除
Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getConfigurations)
.flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new)); // ② 合并所有配置(保序去重)
processedConfigurations.removeAll(allExclusions); // ③ 再排除一次
return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
.map(this::getEntry)
.toList();
}
为什么 ③ 要"再排除一次"?
getAutoConfigurationEntry() 里已经做过 configurations.removeAll(exclusions)(§4.6)。这里是跨 entry 的排除:
// entry A: configurations=[X, Y], exclusions=[]
// entry B: configurations=[Y, Z], exclusions=[X]
// ↑ B 排除了 X
A 贡献了 X,但 B 明确要求排除 X。合并后必须再用并集排除一次 → X 被移除。
这就是"任何一个 @EnableAutoConfiguration 上的 exclude 都是全局生效的"这条语义的实现。
注意 allExclusions 用 Collectors.toSet()(无序 HashSet),而 processedConfigurations 用 LinkedHashSet(保序)。因为排除是集合运算,顺序无关。
7.4 getMetadataReaderFactory():共享缓存
private MetadataReaderFactory getMetadataReaderFactory() {
try {
return this.beanFactory.getBean(SharedMetadataReaderFactoryContextInitializer.BEAN_NAME,
MetadataReaderFactory.class);
}
catch (NoSuchBeanDefinitionException ex) {
return new CachingMetadataReaderFactory(this.resourceLoader);
}
}
SharedMetadataReaderFactoryContextInitializer.BEAN_NAME = "org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory"。
为什么要共享?
CachingMetadataReaderFactory 是一个缓存 ASM 解析结果的工厂。如果 ConfigurationClassPostProcessor 用一个、AutoConfigurationSorter 用另一个,同一个类的字节码会被解析两次。
Boot 通过 SharedMetadataReaderFactoryContextInitializer(一个 ApplicationContextInitializer,在 spring.factories:2-3 注册)注册一个共享的 CachingMetadataReaderFactory,并把它也设置给 ConfigurationClassPostProcessor:
class SharedMetadataReaderFactoryContextInitializer implements
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered, BeanRegistrationExcludeFilter {
public static final String BEAN_NAME = "org.springframework.boot.autoconfigure."
+ "internalCachingMetadataReaderFactory";
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
if (AotDetector.useGeneratedArtifacts()) {
return; // ← AOT 下直接跳过
}
BeanFactoryPostProcessor postProcessor = new CachingMetadataReaderFactoryPostProcessor(applicationContext);
applicationContext.addBeanFactoryPostProcessor(postProcessor);
}
...
}
注意 AOT 下 return —— 因为 AOT 不需要运行时解析字节码。
回退路径:如果拿不到(比如 SharedMetadataReaderFactoryContextInitializer 没生效),就 new CachingMetadataReaderFactory(this.resourceLoader) —— 功能一样,只是不共享。
7.5 Group 是 static final class
private static final class AutoConfigurationGroup
implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
static —— 因为它由 Framework 反射实例化(BeanUtils.instantiateClass),不能是内部类持有外部引用。
private —— 因为只通过 getImportGroup() 暴露 Class 对象,外部不需要引用类型。
8. AutoConfigurationSorter:三轮排序
排序器的目标(类注释):
Sort auto-configuration classes into priority order by reading
@AutoConfigureOrder,@AutoConfigureBeforeand@AutoConfigureAfterannotations (without loading classes).
"without loading classes" 是关键词——见 §6。
8.1 三轮
List<String> getInPriorityOrder(Collection<String> classNames) {
// Initially sort alphabetically
List<String> alphabeticallyOrderedClassNames = new ArrayList<>(classNames);
Collections.sort(alphabeticallyOrderedClassNames);
// Then sort by order
AutoConfigurationClasses classes = new AutoConfigurationClasses(this.metadataReaderFactory,
this.autoConfigurationMetadata, alphabeticallyOrderedClassNames);
List<String> orderedClassNames = new ArrayList<>(classNames);
Collections.sort(orderedClassNames);
orderedClassNames.sort((o1, o2) -> {
int i1 = classes.get(o1).getOrder();
int i2 = classes.get(o2).getOrder();
return Integer.compare(i1, i2);
});
// Then respect @AutoConfigureBefore @AutoConfigureAfter
orderedClassNames = sortByAnnotation(classes, orderedClassNames);
return orderedClassNames;
}
轮 1:字母序归一化
Collections.sort(alphabeticallyOrderedClassNames)
→ 用于构造 AutoConfigurationClasses(保证元数据读取顺序确定)
→ 然后丢弃(只是给构造用)
轮 2:按 @AutoConfigureOrder 稳定排序
orderedClassNames.sort(Comparator.comparingInt(o -> classes.get(o).getOrder()))
→ 注意:List.sort 是稳定排序(TimSort),相同 order 的保持字母序
轮 3:@AutoConfigureBefore / @AutoConfigureAfter 拓扑排序
sortByAnnotation(classes, orderedClassNames)
为什么轮 1 要构造一个"字母序"的 AutoConfigurationClasses?
因为 AutoConfigurationClasses.addToClasses 是递归的(会顺着 before/after 引用去加载其他类的元数据)。如果输入顺序不确定(§2.4),递归的顺序就不确定,而 classes 内部用的是 LinkedHashMap——最终 getAllNames() 的迭代顺序会受影响。
先归一化成字母序,就让整个构造过程确定性化。这是一个精细的工程细节。
8.2 AutoConfigurationClasses:递归收集 + 幽灵类
private void addToClasses(MetadataReaderFactory metadataReaderFactory,
@Nullable AutoConfigurationMetadata autoConfigurationMetadata, Collection<String> classNames,
boolean required) {
for (String className : classNames) {
if (!this.classes.containsKey(className)) {
AutoConfigurationClass autoConfigurationClass = new AutoConfigurationClass(className,
metadataReaderFactory, autoConfigurationMetadata);
boolean available = autoConfigurationClass.isAvailable();
if (required || available) {
this.classes.put(className, autoConfigurationClass);
}
if (available) {
addToClasses(metadataReaderFactory, autoConfigurationMetadata,
autoConfigurationClass.getBefore(), false);
addToClasses(metadataReaderFactory, autoConfigurationMetadata,
autoConfigurationClass.getAfter(), false);
}
}
}
}
两个参数的关键差异:
required = true(顶层调用)→ 无论available与否都放入 maprequired = false(递归调用)→ 只在available时放入
isAvailable() 是什么?
boolean isAvailable() {
try {
if (!wasProcessed()) {
getAnnotationMetadata();
}
return true;
}
catch (Exception ex) {
return false;
}
}
即:能不能读到它的元数据/字节码。读不到(类不在 classpath、字节码损坏)就是 not available。
为什么要递归收集 before/after 引用的类?
考虑:
@AutoConfiguration(after = MissingAutoConfiguration.class)
class MyAutoConfiguration { }
MissingAutoConfiguration 不在候选名单里(也许它的 jar 没引入)。但它出现在 after 里。为了让排序逻辑完整(能判断 MyAutoConfiguration 排在 MissingAutoConfiguration 之后),需要把它加进 classes map——即使它最终不会出现在结果里。
这些就是"幽灵类":参与排序计算,但不出现在输出中。
收尾时会被剔除:
sorted.retainAll(classNames); // ← 只保留原本在 classNames 里的
8.3 轮 3:DFS 拓扑排序
private List<String> sortByAnnotation(AutoConfigurationClasses classes, List<String> classNames) {
List<String> toSort = new ArrayList<>(classNames);
toSort.addAll(classes.getAllNames()); // ← 加入幽灵类
Set<String> sorted = new LinkedHashSet<>();
Set<String> processing = new LinkedHashSet<>();
while (!toSort.isEmpty()) {
doSortByAfterAnnotation(classes, toSort, sorted, processing, null);
}
sorted.retainAll(classNames); // ← 剔除幽灵类
return new ArrayList<>(sorted);
}
private void doSortByAfterAnnotation(AutoConfigurationClasses classes, List<String> toSort, Set<String> sorted,
Set<String> processing, @Nullable String current) {
if (current == null) {
current = toSort.remove(0); // ← 从待排序里取第一个
}
processing.add(current);
Set<String> afters = new TreeSet<>(Comparator.comparing(toSort::indexOf));
afters.addAll(classes.getClassesRequestedAfter(current));
for (String after : afters) {
checkForCycles(processing, current, after);
if (!sorted.contains(after) && toSort.contains(after)) {
doSortByAfterAnnotation(classes, toSort, sorted, processing, after);
}
}
processing.remove(current);
sorted.add(current);
}
这是一个处理 @AutoConfigureBefore/After 约束的 DFS。逐步解析:
toSort.remove(0)取待排序的第一个作为起点(此时已按 order + 字母序排过,第一个就是"最该先排的")processing.add(current)—— 标记"正在处理中",用于环检测classes.getClassesRequestedAfter(current)—— 见下方语义分析- 对集合里每个元素:递归先处理它
- 全部处理完,
sorted.add(current)——current最后加入
关键点:current 在它所依赖的类全部处理完之后才加入 sorted,所以 current 必然排在它们后面。
看 getClassesRequestedAfter 的实现:
Set<String> getClassesRequestedAfter(String className) {
Set<String> classesRequestedAfter = new LinkedHashSet<>(get(className).getAfter());
this.classes.forEach((name, autoConfigurationClass) -> {
if (autoConfigurationClass.getBefore().contains(className)) {
classesRequestedAfter.add(name);
}
});
return classesRequestedAfter;
}
两个来源:
get(className).getAfter()——className自己声明了@AutoConfigureAfter(X),语义是 X 应排在className之前- 遍历所有类,找出声明了
@AutoConfigureBefore(className)的类 —— 语义是该类应排在className之前
两个来源语义一致:都是"应当排在 className 之前、因此需要被优先处理的类"。
于是算法把它们递归先行处理 → 它们先进入 sorted → className 后进入 → className 排在它们之后。语义正确。
但方法名 getClassesRequestedAfter 与它的实际语义相反:名字让人以为"这些类在 X 之后",实际它们在 X 之前。这是一个真实的命名瑕疵(Boot 源码里存在多年)。如果要重构,建议改名为 getClassesRequestedBefore,且必须先跑 AutoConfigurationSorterTests 验证行为不变。
我一开始把这个语义推错了,写出的结论与官方语义矛盾,重读实现后才发现两个来源都是"之前"。§8.6 给出完整推演实例,读者可以自己验算一遍——源码注释没有解释这个算法,推演是唯一可靠的理解途径。
8.4 TreeSet + Comparator.comparing(toSort::indexOf) 的妙用
Set<String> afters = new TreeSet<>(Comparator.comparing(toSort::indexOf));
afters.addAll(classes.getClassesRequestedAfter(current));
用 toSort.indexOf 作比较器:即"按它们在待排序列表中的位置排序"。
为什么? 因为 getClassesRequestedAfter 返回的是 LinkedHashSet,其顺序取决于 classes map 的迭代顺序(字母序 + 递归顺序),不一定符合"order + 字母序"的优先级。用 toSort.indexOf 重新排一遍,保证递归处理时也尊重轮 2 的成果。
TreeSet 的另一个作用:去重(如果 after 里重复出现同一个类)。
一个边界:toSort.indexOf(after) 对不存在的元素返回 -1 → 排在最前面。但外层有 if (... && toSort.contains(after)) 守卫,所以不会发生。
8.5 环检测
private void checkForCycles(Set<String> processing, String current, String after) {
Assert.state(!processing.contains(after),
() -> "AutoConfigure cycle detected between " + current + " and " + after);
}
processing 是"当前递归栈"。如果 after 已经在栈里 → 存在环 → IllegalStateException。
错误信息只报两个类(current 和 after),不报完整环路径。这让复杂环(A→B→C→A)的排查有点困难——你只能看到其中一条边。
真实场景:两个第三方 starter 互相声明 @AutoConfigureAfter。解决方法是去掉其中一个。
8.6 一个完整的排序实例
先看每个类的"应当排在我之前"集合(即 getClassesRequestedAfter,命名相反,见 §8.3):
getClassesRequestedAfter(A):
get(A).getAfter() = {} (A 没声明 after)
遍历找 getBefore().contains(A) 的类 → D
= {D} ← D 应在 A 之前
getClassesRequestedAfter(B) = {}
getClassesRequestedAfter(C):
get(C).getAfter() = {A} (C 声明 @AutoConfigureAfter(A))
遍历找 getBefore().contains(C) → 无
= {A} ← A 应在 C 之前
getClassesRequestedAfter(D) = {}
轮 1:字母序 [A, B, C, D](仅用于构造 classes,保证元数据读取顺序确定)
轮 2:按 order → [B(-100), A(0), C(0), D(0)](A/C/D 同为 0,稳定排序保持字母序)
轮 3:toSort = [B, A, C, D]
round 1: current = B (remove(0))
getClassesRequestedAfter(B) = {} → 无递归
sorted.add(B) → sorted = [B]
toSort = [A, C, D]
round 2: current = A (remove(0))
afters = getClassesRequestedAfter(A) = {D}
TreeSet 按 toSort.indexOf 排序 → [D(3)]
递归 D:
processing = {A, D}
getClassesRequestedAfter(D) = {} → 无递归
sorted.add(D) → sorted = [B, D]
processing = {A}
sorted.add(A) → sorted = [B, D, A]
toSort = [C]
round 3: current = C (remove(0))
afters = {A}
A 已在 sorted 中 → 跳过(`!sorted.contains(after)` 为 false)
sorted.add(C) → sorted = [B, D, A, C]
toSort = []
结果:[B, D, A, C]
验证三条约束:
B最前(order = -100 最小)✅D在A之前(@AutoConfigureBefore(A))✅A在C之前(等价于C在A之后,即C的@AutoConfigureAfter(A))✅
全部满足,算法自洽。
注意 round 3 里 A 已在 sorted 中所以被跳过——这正是 if (!sorted.contains(after) && toSort.contains(after)) 两个守卫的作用:避免重复递归(同时也避免死循环)。
8.7 AutoConfigurations:测试用的排序器
public class AutoConfigurations extends Configurations implements Ordered {
private static final SimpleMetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
private static final int ORDER = AutoConfigurationImportSelector.ORDER;
static final AutoConfigurationReplacements replacements = AutoConfigurationReplacements
.load(AutoConfiguration.class, null);
...
public static AutoConfigurations of(Class<?>... classes) {
return new AutoConfigurations(Arrays.asList(classes));
}
}
这是一个公开 API(public class),用于 ApplicationContextRunner:
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(A.class, B.class))
.run(context -> { ... });
注意:AutoConfigurations.of(...) 会复用同一套 AutoConfigurationSorter(metadataReaderFactory 是静态的、replacements 是静态的),所以测试里的排序行为与生产一致。
ORDER = AutoConfigurationImportSelector.ORDER —— 让测试里的 AutoConfigurations 与真实自动配置导入有同样的优先级。
9. @AutoConfigureBefore / @AutoConfigureAfter 的双向语义
9.1 注解本体
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
public @interface AutoConfigureAfter {
Class<?>[] value() default {};
String[] name() default {};
}
两个属性的分工(与 exclude/excludeName 同理):
value():Class<?>[],会被 ASM 解析(需要类在 classpath)name():String[],纯字符串,安全
Javadoc 明确:
In order to use this annotation as a meta-annotation, only use the
nameattribute.
9.2 排序器怎么读它们
private Set<String> getAnnotationValue(Class<?> annotation) {
Map<String, @Nullable Object> attributes = getAnnotationMetadata()
.getAnnotationAttributes(annotation.getName(), true);
if (attributes == null) {
return Collections.emptySet();
}
Set<String> result = new LinkedHashSet<>();
String[] value = (String[]) attributes.get("value");
String[] name = (String[]) attributes.get("name");
Assert.state(value != null, "'value' must not be null");
Assert.state(name != null, "'name' must not be null");
Collections.addAll(result, value);
Collections.addAll(result, name);
return result;
}
value 和 name 是并集(都收集)。所以:
@AutoConfigureAfter(value = A.class, name = "com.example.B")
→ {A, B}
9.3 一个高频失效场景
"我写了
@AutoConfigureAfter(XxxAutoConfiguration.class),为什么没生效?"
三个可能原因:
① 目标类根本不在候选名单里
排序器会把它当"幽灵类"(§8.2)处理——如果 isAvailable()(类能读到)就参与排序,但最终被 retainAll(classNames) 剔除。这时 @AutoConfigureAfter 的约束仍然会作用(因为幽灵类参与了 DFS),但如果目标类读不到(isAvailable() 返回 false 且 required=false),它就完全不参与排序 → 约束静默失效。
② 目标类被 OnClassCondition 筛掉了
筛掉发生在排序之前(§4.6 的 ⑧ 在 Group 的 selectImports 排序之前)。所以被筛掉的类不在 classNames 里 → 不参与排序 → 你的 @AutoConfigureAfter 无效。
③ 两个类的 order 相差很大
轮 2 先按 @AutoConfigureOrder 排。如果两个类 order 不同,轮 3 的 DFS 不会跨越 order 分组(因为 DFS 是从 toSort 头部开始,而 toSort 已按 order 排好;但递归仍然可能跨组)。实际上轮 3 会改变轮 2 的结果——DFS 的约束优先于 order。
这个优先级值得记住:@AutoConfigureBefore/After > @AutoConfigureOrder > 字母序。
9.4 排序只影响 Bean 定义顺序,不影响创建顺序
AutoConfigureOrder 的 Javadoc 说得很清楚:
As with standard
@Configurationclasses, the order in which auto-configuration classes are applied only affects the order in which their beans are defined. The order in which those beans are subsequently created is unaffected and is determined by each bean's dependencies and any@DependsOnrelationships.
这是一个极其常见的误解:
自动配置顺序 → 决定 BeanDefinition 注册顺序
↓
BeanDefinition 顺序 → 影响:
- @Autowired 按类型注入时的候选顺序(多个同类型候选时,靠前的优先)
- getBeanNamesForType() 的返回顺序
- 同名覆盖时谁"后到"(allowBeanDefinitionOverriding=true 时后者保留)
↓
但不影响:
- Bean 的实例化顺序(由依赖关系决定)
所以:如果你想让 A 在 B 之后实例化,用 @DependsOn 或真实依赖关系,不要用 @AutoConfigureAfter。
反过来,如果你想让"你的 @Bean 覆盖自动配置的 @Bean",@AutoConfigureAfter + 同名是必要的(第 13 章 §13.7 讲过同名覆盖)。
10. AutoConfigurationReplacements:处理"搬家"的自动配置
10.1 文件与格式
final class AutoConfigurationReplacements {
private static final String LOCATION = "META-INF/spring/%s.replacements";
...
String replace(String className) {
return this.replacements.getOrDefault(className, className);
}
}
文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.replacements
格式(Java Properties):
old.package.OldAutoConfiguration=new.package.NewAutoConfiguration
本仓库里目前没有 .replacements 文件(find . -name "*.replacements" 为空)。这是一个预留机制——供 Boot 团队在版本升级时处理"自动配置类被移动/重命名/合并"的情况。
10.2 三个应用场景
① 排除名单的兼容
return getAutoConfigurationReplacements().replaceAll(excluded);
用户写 spring.autoconfigure.exclude=old.OldAutoConfiguration,但类已经搬到 new.NewAutoConfiguration。替换后,排除的是新名字 → 排除生效。
没有这个机制会怎样? 排除无效果(因为新名字不在排除集里),或者触发 checkExcludedClasses 的"无效排除"错误。
② @AutoConfigureBefore/After 的兼容
private Set<String> applyReplacements(Set<String> values) {
if (AutoConfigurationSorter.this.replacementMapper == null) {
return values;
}
Set<String> replaced = new LinkedHashSet<>(values);
for (String value : values) {
replaced.add(AutoConfigurationSorter.this.replacementMapper.apply(value));
}
return replaced;
}
注意这里是"添加"而不是"替换":replaced 同时包含旧名和新名。
为什么? 因为排序时可能遇到任一个名字(取决于 classpath 上是老 jar 还是新 jar)。两个都放进"相关类"集合,无论哪个存在都能正确排序。
这与 ① 的"替换"(getOrDefault)语义不同——排除要用新名(否则排除不到),排序要两个都留(否则漏约束)。
③ AutoConfigurationGroup 的一致性校验
Assert.state(
this.autoConfigurationReplacements == null
|| this.autoConfigurationReplacements.equals(autoConfigurationReplacements),
"Auto-configuration replacements must be the same for each call to process");
AutoConfigurationReplacements 实现了 equals/hashCode(基于 this.replacements map),正是为了这个断言。
10.3 Map.copyOf 与不可变
private AutoConfigurationReplacements(Map<String, String> replacements) {
this.replacements = Map.copyOf(replacements);
}
Map.copyOf(JDK 10+)返回不可变副本,且拒绝 null 键/值。
10.4 与 ImportCandidates 的代码结构对比
ImportCandidates | AutoConfigurationReplacements | |
|---|---|---|
| 位置 | META-INF/spring/{FQN}.imports | META-INF/spring/{FQN}.replacements |
| 格式 | 纯文本,一行一个,# 注释 | Java Properties,key=value |
| 编码 | UTF-8(显式) | UTF-8(显式) |
| 解析 | 手写 BufferedReader | Properties.load |
| 可见性 | public final class | final class(包级私有) |
| 合并 | addAll(累积) | putAll(后者覆盖前者) |
| equals | 无 | 有(用于一致性断言) |
为什么 ImportCandidates 是 public 而 AutoConfigurationReplacements 是包级私有?
因为 ImportCandidates 是公共契约——任何框架都可以声明自己的 .imports(比如 Spring Cloud)。而 .replacements 是 Boot 内部机制,只服务于自动配置。
11. DeferredImportSelector:为什么自动配置必须"延后"
这是全章最重要的概念性结论。
11.1 两种 ImportSelector
Spring Framework 有两个接口:
public interface ImportSelector {
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
public interface DeferredImportSelector extends ImportSelector {
@Nullable default Class<? extends Group> getImportGroup() { return null; }
@Nullable default Predicate<String> getExclusionFilter() { return null; }
}
执行时机差异(ConfigurationClassParser,Framework 行为):
parse(配置类)
→ doProcessConfigurationClass()
├─ 处理 @ComponentScan
├─ 处理 @Import
│ ├─ 普通 ImportSelector → 立即 selectImports() 并递归解析
│ └─ DeferredImportSelector → 加入 deferredImportSelectors 列表
├─ 处理 @Bean
└─ 处理 @ImportResource
→ 【所有配置类处理完后】this.deferredImportSelectorHandler.process()
└─ 按 Group 聚合 → process() × N → selectImports() → 递归解析
11.2 为什么自动配置必须延后
考虑这个场景:
@Configuration
public class MyConfig {
@Bean
public DataSource myDataSource() { ... }
}
和自动配置:
@AutoConfiguration
@ConditionalOnMissingBean(DataSource.class)
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean(DataSource.class)
public DataSource dataSource() { ... }
}
如果 DataSourceAutoConfiguration 用普通 ImportSelector:
- 解析
App(主配置类) - 遇到
@EnableAutoConfiguration→ 立即selectImports() - 得到
DataSourceAutoConfiguration等几百个类 - 立即递归解析
DataSourceAutoConfiguration - 求值
@ConditionalOnMissingBean(DataSource.class) - 此时容器里只有 12 个 Bean 定义(主类 + 注解 processor),没有
myDataSource(因为MyConfig还没被扫描到!) - → 条件通过 →
dataSource()被注册 - 之后扫描发现
MyConfig,注册myDataSource - 结果:两个
DataSourceBean ❌
用 DeferredImportSelector:
- 解析
App @EnableAutoConfiguration→ 记为 deferred,不立即处理- 继续处理
@ComponentScan→ 发现MyConfig→ 注册myDataSource定义 - 继续处理其他
@Import、@Bean - 全部配置类处理完后 → 执行 deferred →
selectImports() - 求值
@ConditionalOnMissingBean(DataSource.class)→ 此时容器里已有myDataSource→ 条件不通过 → 不注册 - 结果:只有用户自己的
DataSource✅
这就是"自动配置总是最后生效、用户配置总是优先"的技术实现。
EnableAutoConfiguration 的 Javadoc 也写了:
Auto-configuration is always applied after user-defined beans have been registered.
11.3 DeferredImportSelector 的排序
Framework 的 DeferredImportSelectorHandler 会对 deferred selectors 按 Ordered / @Order 排序。AutoConfigurationImportSelector.getOrder() 返回 LOWEST_PRECEDENCE - 1,所以自动配置在所有其他 deferred selector 之后执行。
这意味着:如果你写了一个自定义 DeferredImportSelector(比如某个框架),它会先于自动配置执行 → 它的 Bean 会被自动配置的 @ConditionalOnMissingBean 看到。
11.4 一个实践推论:@AutoConfiguration 不能用 @ComponentScan 引入
第 13 章 §12.4.1 讲过 AutoConfigurationExcludeFilter 会阻止自动配置类被 @ComponentScan 扫到。现在可以解释为什么这很重要:
如果自动配置类被 @ComponentScan 扫到,它会作为普通配置类在 doProcessConfigurationClass 里被处理——走的是非 deferred 路径,@ConditionalOnMissingBean 会在"用户 Bean 还没注册"时求值 → 条件失效。
所以 AutoConfigurationExcludeFilter 不是可选的优化,而是正确性的保证。
12. ImportAutoConfiguration:测试切片的基石
12.1 语义
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(ImportAutoConfigurationImportSelector.class)
public @interface ImportAutoConfiguration {
@AliasFor("classes")
Class<?>[] value() default {};
@AliasFor("value")
Class<?>[] classes() default {};
Class<?>[] exclude() default {};
}
Javadoc:
Applies the same ordering rules as
@EnableAutoConfigurationbut restricts the auto-configuration classes to the specified set, rather than consultingImportCandidates.
即:不走 .imports 名单,只导入你指定的那几个。
主要用途(Javadoc 明说):
@ImportAutoConfigurationcan be useful in some situations and especially when writing tests.
这就是 @DataJpaTest、@WebMvcTest 等切片测试的实现基础——它们不需要几百个自动配置,只需要 3-5 个。
12.2 ImportAutoConfigurationImportSelector
class ImportAutoConfigurationImportSelector extends AutoConfigurationImportSelector implements DeterminableImports {
private static final String OPTIONAL_PREFIX = "optional:";
private static final Set<String> ANNOTATION_NAMES;
static {
Set<String> names = new LinkedHashSet<>();
names.add(ImportAutoConfiguration.class.getName());
names.add("org.springframework.boot.autoconfigure.test.ImportAutoConfiguration");
ANNOTATION_NAMES = Collections.unmodifiableSet(names);
}
...
}
三个与父类的差异:
① getAttributes 返回 null
@Override
protected @Nullable AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
return null;
}
父类会 Assert.state(attributes != null),这里直接返回 null → 跳过 exclude/excludeName 的注解读取(改用 getExclusions 的重写版本)。
② getCandidateConfigurations:三种来源
private Collection<String> getConfigurationsForAnnotation(Class<?> source, Annotation annotation) {
String[] classes = (String[]) AnnotationUtils.getAnnotationAttributes(annotation, true).get("classes");
if (classes != null && classes.length > 0) {
return Arrays.asList(classes); // ① 注解里直接列的类
}
return loadFactoryNames(source).stream().map(this::mapFactoryName).filter(Objects::nonNull).toList();
}
protected Collection<String> loadFactoryNames(Class<?> source) {
return ImportCandidates.load(source, getBeanClassLoader()).getCandidates(); // ② 按 source 类名找 .imports
}
来源 ② 是关键设计:
@ImportAutoConfiguration // 在 com.example.MyTest 上
class MyTest { }
如果注解没列 classes,就去读 META-INF/spring/com.example.MyTest.imports。
这就是切片注解的实现方式:
META-INF/spring/org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper.imports
(类似的命名,具体路径依版本而定)
optional: 前缀:
private @Nullable String mapFactoryName(String name) {
if (!name.startsWith(OPTIONAL_PREFIX)) {
return name;
}
name = name.substring(OPTIONAL_PREFIX.length());
return (!present(name)) ? null : name;
}
private boolean present(String className) {
String resourcePath = ClassUtils.convertClassNameToResourcePath(className) + ".class";
return new ClassPathResource(resourcePath).exists();
}
optional:com.example.Foo → 如果 Foo 不在 classpath,静默忽略。
注意判定方式是"检查 .class 资源是否存在,而不是 ClassUtils.isPresent(后者会真的加载类)。这对"类存在但依赖缺失"的情况更宽容。
③ determineImports:AOT 支持
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
List<String> candidateConfigurations = getCandidateConfigurations(metadata, null);
Set<String> result = new LinkedHashSet<>(candidateConfigurations);
result.removeAll(getExclusions(metadata, null));
return Collections.unmodifiableSet(result);
}
DeterminableImports(第 13 章 §14.5 讲过)让 AOT 能在构建期"预测"导入结果,而不必真的跑一遍。
注意它没跑 ConfigurationClassFilter —— AOT 阶段不做条件预筛(那是运行时的类路径相关行为)。
12.3 ANNOTATION_NAMES 里的第二个名字
names.add("org.springframework.boot.autoconfigure.test.ImportAutoConfiguration");
这是一个兼容性别名——spring-boot-test-autoconfigure 里有一个同名的注解(包名不同),用于向后兼容。硬编码字符串而不是 Class 引用,是为了避免 spring-boot-autoconfigure 依赖 spring-boot-test-autoconfigure(依赖方向不能反)。
这是一个"用字符串打破循环依赖"的实用技巧。
13. AOT 与自动配置
13.1 运行时跳过
第 13 章 §17 讲过 AOT 模式下 load() 被跳过。自动配置也类似——AOT 生成的 __ApplicationContextInitializer 会把所有 Bean 定义(含自动配置筛选后的结果)写成 Java 源码。
那么构建期是怎么做的?
SpringApplicationAotProcessor 会真的跑一遍上下文(但不 refresh() 到实例化阶段),在 ConfigurationClassPostProcessor 里捕获自动配置的结果,然后生成等价的 Java 代码。
13.2 DeterminableImports 的作用
ImportAutoConfigurationImportSelector 实现了它,AutoConfigurationImportSelector 没有。
为什么? 因为 AutoConfigurationImportSelector 的结果依赖:
- classpath 上的
.imports文件(运行时 vs 构建期可能不同) OnClassCondition的求值(运行时 classpath)
所以它不可确定,AOT 只能"实际跑一遍并快照结果"。而 ImportAutoConfiguration 的候选是注解里写死的,可以确定。
13.3 构建期与运行期的一致性
AOT 引擎会比较构建期与运行期的 determineImports() 结果。如果 @ImportAutoConfiguration 的候选变了(比如 classpath 不同),会报错而不是静默用陈旧结果。
这是一个重要的安全设计:AOT 的"静态化"是假设,一旦假设不成立必须暴露。
13.4 SharedMetadataReaderFactoryContextInitializer 在 AOT 下跳过
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
if (AotDetector.useGeneratedArtifacts()) {
return;
}
...
}
因为 AOT 下不需要运行时解析字节码(已静态化)。
14. 端到端追踪:一次完整的自动配置导入
假设应用引入了 spring-boot-starter-web + spring-boot-starter-jdbc,主类 com.example.app.App。
阶段 1:refresh() → invokeBeanFactoryPostProcessors()
1. ConfigurationClassPostProcessor(BeanDefinitionRegistryPostProcessor)
2. postProcessBeanDefinitionRegistry()
3. → processConfigBeanDefinitions()
4. → parser.parse(candidates)
candidates = [app (主类), org.springframework.context.annotation.internalConfigurationAnnotationProcessor 等]
阶段 2:解析 app
doProcessConfigurationClass(App):
├─ @ComponentScan → Scanner B 扫 com.example.app(第 13 章 §12)
│ exclude: TypeExcludeFilter + AutoConfigurationExcludeFilter
│ → 注册用户的 @Component / @Service / @Configuration
├─ @Import(AutoConfigurationImportSelector.class)
│ → 判定为 DeferredImportSelector
│ → deferredImportSelectors.add(...)
├─ @Import(AutoConfigurationPackages.Registrar.class)
│ → ImportBeanDefinitionRegistrar → 立即执行
│ → AutoConfigurationPackages.register(registry, "com.example.app")
└─ @Bean 方法处理(若有)
阶段 3:deferredImportSelectorHandler.process()
1. 按 @Order 排序 deferred selectors
→ AutoConfigurationImportSelector (LOWEST_PRECEDENCE - 1) 排在最后
2. 取 getImportGroup() → AutoConfigurationGroup.class
3. 创建 Group 实例(反射),注入 BeanClassLoader / BeanFactory / ResourceLoader
4. group.process(app 的 metadata, selector)
├─ Assert: selector 是 AutoConfigurationImportSelector
├─ 校验 replacements 一致
├─ selector.getAutoConfigurationEntry(metadata)
│ ├─ isEnabled → spring.boot.enableautoconfiguration 未设置 → true
│ ├─ getAttributes → exclude/excludeName(空)
│ ├─ getCandidateConfigurations
│ │ └─ ImportCandidates.load(AutoConfiguration.class, cl)
│ │ → 读 classpath 上所有 .imports
│ │ → 假设得到 187 个候选(12 核心 + 各模块)
│ ├─ removeDuplicates → 185(假设有 2 个重复)
│ ├─ getExclusions → 空
│ ├─ checkExcludedClasses → 无
│ ├─ removeAll → 185
│ ├─ getConfigurationClassFilter().filter(185)
│ │ ├─ 加载 spring-autoconfigure-metadata.properties
│ │ ├─ OnClassCondition(@Order HIGHEST_PRECEDENCE,最先)
│ │ │ → 并行(候选 > 1 且多核)→ 筛掉没对应依赖的
│ │ │ → 假设筛掉 120 个 → 65 个
│ │ ├─ OnBeanCondition → 筛掉 3 个 → 62
│ │ └─ OnWebApplicationCondition → 筛掉 4 个 → 58
│ └─ fireAutoConfigurationImportEvents
│ → ConditionEvaluationReportAutoConfigurationImportListener
│ → report.recordEvaluationCandidates(58)
│ → report.recordExclusions(空)
└─ entries.putIfAbsent(每个类名, app 的 metadata) × 58
5. group.selectImports()
├─ allExclusions = 空
├─ processedConfigurations = 58(LinkedHashSet 保序)
├─ removeAll(空) → 58
├─ getAutoConfigurationMetadata() → 加载/复用元数据
├─ sortAutoConfigurations(58)
│ └─ AutoConfigurationSorter.getInPriorityOrder(58)
│ 轮 1:字母序
│ 轮 2:@AutoConfigureOrder
│ 轮 3:@AutoConfigureBefore/After DFS
└─ map(getEntry) → 58 个 Entry(metadata, className)
6. 对每个 Entry 递归 parse()
→ 求值类上的 @ConditionalOnXxx(正式求值,第 15 章)
→ 处理 @Bean 方法
→ 处理嵌套 @Import
阶段 4:结果
最终 BeanDefinition 数量 ≈
6(Spring 内部 processor)
+ 1(springApplicationArguments)
+ 1(springBootBanner)
+ 1(app)
+ N(用户 @Component)
+ 1(AutoConfigurationPackages 的 BasePackages)
+ M(58 个自动配置里通过正式条件求值的那些)
+ K(这些自动配置里的 @Bean 方法)
典型 Spring Boot Web + JPA 应用:300–500 个。
阶段 5:诊断
访问 /actuator/conditions 或启动时 --debug:
positiveMatches: 生效的自动配置(含匹配原因)
negativeMatches: 未生效的(含不匹配原因)
exclusions: 被显式排除的
unconditionalClasses: 无条件的配置类
这三个分区正好对应本章的三个阶段:
unconditionalClasses← 没被 filter 筛、也没条件注解的negativeMatches← 被 filter 筛掉(预筛)+ 被正式求值判死exclusions←getExclusions()的产物
15. 反直觉结论汇总(18 条)
15.1 候选名单的原始顺序不可依赖
ImportCandidates 只是按 classpath 枚举顺序累积,不做排序。所有顺序保证都来自 AutoConfigurationSorter。
15.2 排序器第一轮"字母序"是为了确定性,不是为了好看
它让后续的递归元数据采集顺序可复现。
15.3 getClassesRequestedAfter(X) 的命名与实际语义相反
它返回的是"应当排在 X 之前的类"。算法正确,命名误导。
15.4 排序器会引入"幽灵类"
@AutoConfigureAfter 引用了但不在候选名单里的类,会参与排序计算(只要能读到元数据),最后被 retainAll 剔除。
15.5 @AutoConfigureBefore/After 的优先级 高于 @AutoConfigureOrder
轮 3 的 DFS 可以改变轮 2 的结果。
15.6 排序只影响 Bean 定义顺序,不影响 Bean 创建顺序
想控制创建顺序用 @DependsOn 或真实依赖。
15.7 .imports 里是去中心化的,每个模块自带
这是 4.x 相对 3.x spring.factories 的核心改进:不引入 jar 就没有候选。
15.8 但 AutoConfigurationImportFilter / Listener 仍在 spring.factories
因为它们需要实例化 + @Order 排序,SpringFactoriesLoader.loadFactories 更合适。(推演)
15.9 预筛(filter)与正式求值用的是同一批类,但不同的代码路径
FilteringSpringBootCondition 同时实现 Condition 和 AutoConfigurationImportFilter。
15.10 OnClassCondition 只开一个额外线程
Boot 团队的实测结论:开更多线程反而更慢。注释里写明了。
15.11 AutoConfigurationMetadata 缺失时功能仍正确,只是慢
wasProcessed() 为 false 就退回读字节码。这是优雅降级。
15.12 match() 数组可以含 null,实现不能修改它
接口注释明确要求。Boot 用"置 null"而不是 remove 来避免索引错乱。
15.13 排除检查(checkExcludedClasses)在 filter 之前
否则"排除一个被条件判死的类"会被误报为"无效排除"。
15.14 spring.boot.enableautoconfiguration 只对精确类生效
子类(ImportAutoConfigurationImportSelector)永远返回 true。(getClass() == ... 守卫)
15.15 DeferredImportSelector 是"用户 Bean 优先"的唯一技术实现
不用它,@ConditionalOnMissingBean 会在用户 Bean 注册前求值,导致条件失效。
15.16 AutoConfigurationExcludeFilter 是正确性保证,不是优化
如果自动配置类被 @ComponentScan 扫到,会走非 deferred 路径 → 条件失效。
15.17 Group 的价值是跨多个 @EnableAutoConfiguration 的全局去重与排序
且"任何一个 entry 的 exclude 都是全局生效的"(selectImports 里的二次 removeAll)。
15.18 AutoConfigurationReplacements 对"排除"是替换,对"排序"是累加
排除必须用新名(否则排不到),排序要新旧名都留(否则漏约束)。
16. 动手验证清单
16.1 打印候选与筛选结果
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
加 JVM 参数 -Dlogging.level.org.springframework.boot.autoconfigure=TRACE,观察:
Filtered 120 auto configuration class in 42 ms
16.2 列出全部候选(绕过筛选)
ImportCandidates candidates = ImportCandidates.load(
AutoConfiguration.class, getClass().getClassLoader());
System.out.println(candidates.getCandidates().size());
candidates.getCandidates().forEach(System.out::println);
16.3 验证 exclude 的防呆
@SpringBootApplication(exclude = {String.class}) // ❌ String 不是自动配置类
期望:启动失败,IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes: - java.lang.String
对照:
@SpringBootApplication(excludeName = {"com.example.NotOnClasspath"})
期望:不报错(因为 ClassUtils.isPresent 为 false)。
16.4 验证 exclude 的全局性
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class App { }
@Configuration
@EnableAutoConfiguration // 第二个,没有 exclude
class Second { }
期望:DataSourceAutoConfiguration 仍被排除(Group 的 allExclusions 并集)。
16.5 验证排序
@AutoConfiguration
@AutoConfigureAfter(name = "com.example.C")
static class A { }
@AutoConfiguration
@AutoConfigureBefore(name = "com.example.A")
static class B { }
@AutoConfiguration
static class C { }
用 ApplicationContextRunner.withConfiguration(AutoConfigurations.of(A.class, B.class, C.class)),打印 Bean 定义顺序。
按 §8.6 的算法推演:
- 轮 2:order 全 0 → 字母序
[A, B, C] - 轮 3:
- current=A,
getClassesRequestedAfter(A)=A.after={C}+B.before 含 A→{C, B},按toSort.indexOf排 =[B(1), C(2)] - 递归 B →
getClassesRequestedAfter(B)={}→sorted=[B] - 递归 C →
getClassesRequestedAfter(C)=A.after 含 C?不,getAfter()是 C 自己的 after(空);找getBefore().contains(C)→ A(A.after不算 before)→ 无 →{}→sorted=[B, C] sorted.add(A)→[B, C, A]
- current=A,
- 结果:
[B, C, A]
验证语义:B 在 A 前 ✅(@AutoConfigureBefore(A));C 在 A 前 ✅(A 声明 @AutoConfigureAfter(C))。
16.6 验证 DeferredImportSelector 的必要性
@Configuration
public class UserConfig {
@Bean
public MyService myService() { return new MyService("user"); }
}
@AutoConfiguration
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService autoService() { return new MyService("auto"); }
}
确保 MyAutoConfiguration 在 .imports 里(走 deferred)。运行后 ctx.getBean(MyService.class).getName() → "user"。
关键对照:把 MyAutoConfiguration 不放进 .imports,而是手动 @Import(MyAutoConfiguration.class)(普通 import,非 deferred)→ 结果会变成 两个 Bean(或抛异常,取决于配置)。
这个实验直接证明了 §11.2 的论点。
16.7 验证并行筛选
在单核环境(如限制 CPU 的容器)与多核环境分别启动,观察日志时间与 resolveOutcomesThreaded 是否被走到。可通过在 OnClassCondition.getOutcomes 打断点验证。
16.8 验证 spring.autoconfigure.exclude 的两种写法
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
spring:
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
两者都有效(Binder 对 String[] 支持逗号与列表,第 08 章)。
16.9 观察 ConditionEvaluationReport
启动时加 --debug,或访问 /actuator/conditions。比对:
positiveMatches中的自动配置(生效)negativeMatches中的(含 "Did not find class" / "Not a web application" 等原因)exclusions
16.10 验证 optional: 前缀
创建 META-INF/spring/com.example.MyMarker.imports:
optional:com.example.Optional1
com.example.Required1
@ImportAutoConfiguration 在 MyMarker 上,且 Optional1 不存在、Required1 存在。
期望:只导入 Required1,不报错。
17. 源码索引
| 主题 | 文件 | 关键行 |
|---|---|---|
| 选择器主体 | .../autoconfigure/AutoConfigurationImportSelector.java | 78–81 类声明;81 ORDER;142–155 getAutoConfigurationEntry |
| 候选读取 | 同上 | 200–210 getCandidateConfigurations |
| 排除 | 同上 | 247–255 getExclusions;212–238 checkExcludedClasses |
| 属性排除 | 同上 | 263–276 getExcludeAutoConfigurationsProperty |
| 预筛 | 同上 | 388–429 ConfigurationClassFilter |
| Aware 转发 | 同上 | 329–344 invokeAwareMethods |
| Group | 同上 | 431–539 AutoConfigurationGroup |
| Entry | 同上 | 541–571 |
| 候选人文件 | org/springframework/boot/context/annotation/ImportCandidates.java | 42 LOCATION;82 load;110–135 解析 |
| 替换表 | .../autoconfigure/AutoConfigurationReplacements.java | 41 LOCATION;56–65 replaceAll/replace |
| 排序器 | .../autoconfigure/AutoConfigurationSorter.java | 65–82 三轮;84–94 DFS;159–167 getClassesRequestedAfter |
| 元数据 | .../autoconfigure/AutoConfigurationMetadataLoader.java | 36 PATH |
| 元数据接口 | .../autoconfigure/AutoConfigurationMetadata.java | 全文 |
| 过滤器接口 | .../autoconfigure/AutoConfigurationImportFilter.java | 全文 |
| 监听器接口 | .../autoconfigure/AutoConfigurationImportListener.java | 全文 |
| 事件 | .../autoconfigure/AutoConfigurationImportEvent.java | 全文 |
| 条件基类 | .../autoconfigure/condition/FilteringSpringBootCondition.java | 47–68 match |
| 类条件 | .../autoconfigure/condition/OnClassCondition.java | 45 @Order;48–63 getOutcomes;64–78 并行 |
| 报告监听 | .../autoconfigure/condition/ConditionEvaluationReportAutoConfigurationImportListener.java | 全文 |
| 启用注解 | .../autoconfigure/EnableAutoConfiguration.java | 71 ENABLED_OVERRIDE_PROPERTY;78–81 |
| 自动配置注解 | .../autoconfigure/AutoConfiguration.java | 62–63 |
| 顺序注解 | .../autoconfigure/AutoConfigureOrder.java | 55 DEFAULT_ORDER |
| Before/After | .../autoconfigure/AutoConfigureBefore.java / AutoConfigureAfter.java | 全文 |
| 测试导入 | .../autoconfigure/ImportAutoConfiguration.java + ImportAutoConfigurationImportSelector.java | 全文 |
| 测试工具 | .../autoconfigure/AutoConfigurations.java | 全文 |
| 元数据工厂共享 | .../autoconfigure/SharedMetadataReaderFactoryContextInitializer.java | 60–66 initialize |
| SPI 注册 | core/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories | 11–19 |
| 核心名单 | core/spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports | 12 行 |
未能从本仓库核实的部分(Spring Framework 7.0.9 行为):
ConfigurationClassParser对DeferredImportSelector的处理流程与时机DeferredImportSelectorHandler的 Group 聚合算法DeferredImportSelector.Group.Entry的语义ConfigurationClassPostProcessor与SharedMetadataReaderFactory的绑定细节
18. 本章未展开的悬念
| 悬念 | 将在 |
|---|---|
@ConditionalOnClass / OnMissingBean / OnProperty 的正式求值细节 | 第 15 章 |
| "条件求值顺序会影响结果"这一陷阱的完整分析 | 第 15 章 |
ConditionEvaluationReport 的数据结构与输出格式 | 第 15 章 |
OnBeanCondition 的"Bean 定义阶段"判定 | 第 15 章 |
ConfigurationClassPostProcessor 的完整解析循环 | 第 15 章 |
| AOT 生成代码的自动配置部分 | 第 27 章 |
@WebMvcTest 等切片如何用 ImportAutoConfiguration | 第 28 章 |
spring-boot-actuator 的 /actuator/conditions 端点 | 第 24 章 |
19. 本章未逐行核实 / 待你实测的项
AutoConfigurationImportSelector在不同上下文类型下的差异未逐个验证。AutoConfigurationSorter三轮排序的实现细节在第 16 章补充,本章只给结论。AutoConfigurationMetadata的生成与消费链路未逐行读。@AutoConfiguration的before/after冲突检测规则未穷举。- AOT 下 import 列表的来源在第 28 章。
20. 下一章预告
第 14 章讲了自动配置类的"导入":怎么找到候选、怎么预筛、怎么排序。
但注意一个关键事实:导入 ≠ 生效。
被 selectImports() 返回的 58 个类,还会经历第二轮条件求值——ConfigurationClassParser 会逐个解析它们,求值类上的 @ConditionalOnXxx 注解。只有通过的,其 @Bean 方法才会被注册。
这一轮与预筛的区别:
| 预筛(本章) | 正式求值(第 15 章) | |
|---|---|---|
| 执行者 | AutoConfigurationImportFilter | ConditionEvaluator |
| 输入 | AutoConfigurationMetadata(properties) | 类的注解(ASM / 反射) |
| 粒度 | 批量,一次几百个 | 逐个 |
| 时机 | selectImports() 之前 | selectImports() 之后 |
| 能否看到用户 Bean | 部分(OnBeanCondition 可以,但此时 Bean 定义还不完整) | 完整 |
| 结果记录 | 记进 report 的 candidates | 记进 positiveMatches / negativeMatches |
第 15 章会拆开:
SpringBootCondition的模板方法:getMatchOutcome+ 日志 + 报告OnClassCondition/OnBeanCondition/OnPropertyCondition/OnResourceCondition/OnWebApplicationCondition逐个@ConditionalOnMissingBean的"顺序敏感"陷阱:为什么两个自动配置的相对顺序会改变结果ConditionEvaluationReport:--debug与/actuator/conditions背后的数据结构- 条件注解的元注解与组合:
@ConditionalOnWebApplication之类的自定义条件 - 条件求值失败的排查手册
第 15 章:条件注解体系 —— 全部 @ConditionalOnXxx 的实现原理。