Spring Boot 4.1.1 深度源码解析-第 14 章 · 自动配置引擎:AutoConfigurationImportSelector 全流程
前情提要 :第 13 章把"原始 Bean 定义从哪来"讲完了。我们留下一个空洞------
@EnableAutoConfiguration是怎么变成 100+ 个自动配置类的?本章把它填满。
0. 本章要回答的六个问题
java
@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):
java
@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 {};
}
它只做了三件事:
less
@EnableAutoConfiguration
│
├─ ① @AutoConfigurationPackage
│ └─ @Import(AutoConfigurationPackages.Registrar)
│ → 记录主类所在包(第 13 章 §14)
│
├─ ② @Import(AutoConfigurationImportSelector.class)
│ └─ 本章全部内容
│
└─ ③ 声明 exclude / excludeName 两个属性
└─ 供 AutoConfigurationImportSelector 读取
注意 ③ :属性本身不带任何逻辑 。它们是"数据",由 AutoConfigurationImportSelector 用 AnnotationMetadata 读出来。这是 Spring 注解体系的一贯风格------注解是元数据载体,逻辑在处理器里。
1.1 ENABLED_OVERRIDE_PROPERTY
java
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
这个常量的名字里有个 spring.boot 前缀------注意不是 spring.autoconfigure。这是一个历史遗留的命名不一致(Boot 1.x 就这么叫了)。
用法:
yaml
spring:
boot:
enableautoconfiguration: false
但它在 2.1 被弃用后又被"半复活"了------现在的语义见 §3.2。
1.2 与 @SpringBootApplication 的关系
java
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { ... })
public @interface SpringBootApplication { ... }
@SpringBootApplication 元标注 了 @EnableAutoConfiguration。所以:
AutoConfigurationImportSelector的getAttributes()读的是EnableAutoConfiguration的属性(不是SpringBootApplication的)- 而
@SpringBootApplication用@AliasFor(annotation = EnableAutoConfiguration.class)把exclude/excludeName别名过去(第 13 章讲过)
所以这两种写法等价:
java
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@SpringBootApplication(excludeName = "org.springframework.boot...DataSourceAutoConfiguration")
2. ImportCandidates:名单从哪来
2.1 位置常量
java
// ImportCandidates.java: 42
private static final String LOCATION = "META-INF/spring/%s.imports";
%s 是注解的全限定名。对于自动配置:
bash
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
对应代码(AutoConfigurationImportSelector.java:202):
java
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, getBeanClassLoader());
其中 this.autoConfigurationAnnotation 默认是 AutoConfiguration.class(构造行 113--116):
java
AutoConfigurationImportSelector(@Nullable Class<?> autoConfigurationAnnotation) {
this.autoConfigurationAnnotation = (autoConfigurationAnnotation != null) ? autoConfigurationAnnotation
: AutoConfiguration.class;
}
这个字段的存在是为了子类化 ------ImportAutoConfigurationImportSelector 就传了别的值(§13)。
2.2 本仓库的实际规模
lua
$ 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 读取逻辑
java
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("#")------ 行内注释也支持 :bashcom.example.Foo # 这是一条注释会被截断成
com.example.Foo。这一点与.properties的#规则不同(后者只在行首)。 -
trim()后跳过空行 -
不做任何去重 ------ 重复的类名会留在列表里,交给
removeDuplicates()处理(§4.3) -
不做任何排序 ------ 顺序就是 classpath 枚举顺序 × 文件内行顺序
2.4 【重要】候选顺序是"不确定的"
java
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 空名单的硬断言
java
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 类声明
java
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:那个"半复活"的开关
java
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 都检查它:
java
@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()
java
@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
java
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):
java
@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):
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 行,我这里按语义拆成 9 步。)
4.1 ② getAttributes
java
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
java
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
java
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:三个来源
java
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 的绑定
java
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)→ 只支持逗号分隔
元数据里声明的类型与实际不符:
json
{
"name": "spring.autoconfigure.exclude",
"type": "java.util.List<java.lang.Class>",
"description": "Auto-configuration classes to exclude."
}
元数据写的是 List<Class>,但代码绑的是 String[]。这是一个元数据与实现的小不一致------元数据只影响 IDE 提示,不影响运行时。实践中写 YAML 列表或逗号串都可以。
④ 替换映射
java
return getAutoConfigurationReplacements().replaceAll(excluded);
见 §11。
4.5 ⑥ checkExcludedClasses:防呆检查
java
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 才是真正的判定
这个检查的价值:防止"写错了类名但静默无效"。经典场景:
java
@SpringBootApplication(exclude = {DataSourceAutoConfig.class}) // ← 少了个 uration
如果没有这个检查,你以为排除了,实际没有,然后花三小时查为什么数据源还是被创建了。
一个反直觉的边界:
java
@SpringBootApplication(exclude = {SomeClass.class}) // SomeClass 是一个自动配置类,但被 spring.autoconfigure.exclude 之外的方式排除了
没问题,因为检查的是完整候选名单 (configurations 是 candidates,还没被 filter 筛)。
但另一个边界会出问题:
如果你排除的类所在的 jar 引入后,它的 .imports 又因为某种原因没被读到(比如候选为空触发了 §2.5 的断言),那么......实际上会先触发断言,不会到这里。
4.6 ⑦ removeAll 与 ⑧ filter
java
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
顺序很重要:先排除用户显式指定的,再用条件 filter 筛。
为什么不能反过来? 如果先 filter,checkExcludedClasses 看到的 configurations 就是"筛选后"的名单,那么"排除一个被 OnClassCondition 判死的类"就会被误判为"无效排除"→ 报错。先排除再 filter 就没这个问题。
4.7 ⑨ fireAutoConfigurationImportEvents
java
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):
ini
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
它的实现(本仓库):
java
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 的结构
java
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)
ini
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 的实现
java
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 看到的索引始终一致。
这是接口注释要求的:
java
/**
* @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
java
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 的两副面孔
java
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 的并行优化
java
@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 调用 (微秒级)。开更多线程的同步/调度开销超过了并行收益。这与"并行越多越快"的直觉相反------这是实测得出的结论,不是理论推导。
注意执行顺序的巧妙:
java
@Nullable ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes(); // 先同步算后半
@Nullable ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes(); // 再取前半(此时后台已完成)
firstHalfResolver 是 ThreadedOutcomesResolver,构造时就启动后台线程 开始算前半段。然后主线程同步算后半段。最后 firstHalfResolver.resolveOutcomes() 阻塞等待后台结果(通常已完成)。
这是一个标准的 fork-join 手写实现,把"主线程也利用起来",而不是让主线程干等。
触发条件 :length > 1 && availableProcessors() > 1。单核或只有一个候选时不走并行。
6. AutoConfigurationMetadata:避免读字节码的关键
6.1 它是什么
java
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 生成):
properties
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 的两条路径
java
@Override
public boolean wasProcessed(String className) {
return this.properties.containsKey(className);
}
在 AutoConfigurationSorter.AutoConfigurationClass 里:
java
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 的类型转换
java
@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 的意义
java
@Override
public Class<? extends Group> getImportGroup() {
return AutoConfigurationGroup.class;
}
Spring Framework 的 DeferredImportSelector 可以返回一个 Group 实现类。语义是:
同一个 Group 的所有
DeferredImportSelector的process()会先被全部调用,最后统一调用一次selectImports()。
为什么自动配置需要这个?
因为可能有多个 @EnableAutoConfiguration:
java
@SpringBootApplication // ← 第一个
public class App { }
@Configuration
@EnableAutoConfiguration // ← 第二个(比如从某个库引入)
class SomeLibConfig { }
或者测试里:
java
@SpringBootTest
@ImportAutoConfiguration(...) // ← 另一个 selector
没有 Group,每个 selector 独立 selectImports(),会产生多个独立的导入批次 ------各自去重、各自排序,最终顺序混乱,且 @AutoConfigureAfter 的跨批次依赖失效。
有 Group,所有 selector 的贡献被聚合后统一处理一次 → 全局去重、全局排序。
7.2 process():聚合并校验
java
@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():三步产出
java
@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 的排除:
java
// 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():共享缓存
java
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:
java
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
java
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 三轮
java
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;
}
kotlin
轮 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:递归收集 + 幽灵类
java
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() 是什么?
java
boolean isAvailable() {
try {
if (!wasProcessed()) {
getAnnotationMetadata();
}
return true;
}
catch (Exception ex) {
return false;
}
}
即:能不能读到它的元数据/字节码。读不到(类不在 classpath、字节码损坏)就是 not available。
为什么要递归收集 before/after 引用的类?
考虑:
java
@AutoConfiguration(after = MissingAutoConfiguration.class)
class MyAutoConfiguration { }
MissingAutoConfiguration 不在候选名单里 (也许它的 jar 没引入)。但它出现在 after 里。为了让排序逻辑完整(能判断 MyAutoConfiguration 排在 MissingAutoConfiguration 之后),需要把它加进 classes map------即使它最终不会出现在结果里。
这些就是"幽灵类":参与排序计算,但不出现在输出中。
收尾时会被剔除:
java
sorted.retainAll(classNames); // ← 只保留原本在 classNames 里的
8.3 轮 3:DFS 拓扑排序
java
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 的实现:
java
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) 的妙用
java
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 环检测
java
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):
scss
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]
ini
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:测试用的排序器
java
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:
java
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 注解本体
java
@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 排序器怎么读它们
java
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 是并集(都收集)。所以:
java
@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.
这是一个极其常见的误解:
ini
自动配置顺序 → 决定 BeanDefinition 注册顺序
↓
BeanDefinition 顺序 → 影响:
- @Autowired 按类型注入时的候选顺序(多个同类型候选时,靠前的优先)
- getBeanNamesForType() 的返回顺序
- 同名覆盖时谁"后到"(allowBeanDefinitionOverriding=true 时后者保留)
↓
但不影响:
- Bean 的实例化顺序(由依赖关系决定)
所以 :如果你想让 A 在 B 之后实例化 ,用 @DependsOn 或真实依赖关系,不要用 @AutoConfigureAfter。
反过来,如果你想让"你的 @Bean 覆盖自动配置的 @Bean",@AutoConfigureAfter + 同名是必要的(第 13 章 §13.7 讲过同名覆盖)。
10. AutoConfigurationReplacements:处理"搬家"的自动配置
10.1 文件与格式
java
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):
properties
old.package.OldAutoConfiguration=new.package.NewAutoConfiguration
本仓库里目前没有 .replacements 文件 (find . -name "*.replacements" 为空)。这是一个预留机制------供 Boot 团队在版本升级时处理"自动配置类被移动/重命名/合并"的情况。
10.2 三个应用场景
① 排除名单的兼容
java
return getAutoConfigurationReplacements().replaceAll(excluded);
用户写 spring.autoconfigure.exclude=old.OldAutoConfiguration,但类已经搬到 new.NewAutoConfiguration。替换后,排除的是新名字 → 排除生效。
没有这个机制会怎样? 排除无效果(因为新名字不在排除集里),或者触发 checkExcludedClasses 的"无效排除"错误。
② @AutoConfigureBefore/After 的兼容
java
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 的一致性校验
java
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 与不可变
java
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 有两个接口:
java
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 行为):
scss
parse(配置类)
→ doProcessConfigurationClass()
├─ 处理 @ComponentScan
├─ 处理 @Import
│ ├─ 普通 ImportSelector → 立即 selectImports() 并递归解析
│ └─ DeferredImportSelector → 加入 deferredImportSelectors 列表
├─ 处理 @Bean
└─ 处理 @ImportResource
→ 【所有配置类处理完后】this.deferredImportSelectorHandler.process()
└─ 按 Group 聚合 → process() × N → selectImports() → 递归解析
11.2 为什么自动配置必须延后
考虑这个场景:
java
@Configuration
public class MyConfig {
@Bean
public DataSource myDataSource() { ... }
}
和自动配置:
java
@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 语义
java
@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
java
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
java
@Override
protected @Nullable AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
return null;
}
父类会 Assert.state(attributes != null),这里直接返回 null → 跳过 exclude/excludeName 的注解读取(改用 getExclusions 的重写版本)。
② getCandidateConfigurations:三种来源
java
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
}
来源 ② 是关键设计:
java
@ImportAutoConfiguration // 在 com.example.MyTest 上
class MyTest { }
如果注解没列 classes,就去读 META-INF/spring/com.example.MyTest.imports。
这就是切片注解的实现方式:
bash
META-INF/spring/org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTestContextBootstrapper.imports
(类似的命名,具体路径依版本而定)
optional: 前缀:
java
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 支持
java
@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 里的第二个名字
java
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 下跳过
java
@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()
scss
1. ConfigurationClassPostProcessor(BeanDefinitionRegistryPostProcessor)
2. postProcessBeanDefinitionRegistry()
3. → processConfigBeanDefinitions()
4. → parser.parse(candidates)
candidates = [app (主类), org.springframework.context.annotation.internalConfigurationAnnotationProcessor 等]
阶段 2:解析 app
java
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()
dart
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:结果
diff
最终 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:诊断
yaml
访问 /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 打印候选与筛选结果
java
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
加 JVM 参数 -Dlogging.level.org.springframework.boot.autoconfigure=TRACE,观察:
arduino
Filtered 120 auto configuration class in 42 ms
16.2 列出全部候选(绕过筛选)
java
ImportCandidates candidates = ImportCandidates.load(
AutoConfiguration.class, getClass().getClassLoader());
System.out.println(candidates.getCandidates().size());
candidates.getCandidates().forEach(System.out::println);
16.3 验证 exclude 的防呆
java
@SpringBootApplication(exclude = {String.class}) // ❌ String 不是自动配置类
期望 :启动失败,IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes: - java.lang.String
对照:
java
@SpringBootApplication(excludeName = {"com.example.NotOnClasspath"})
期望 :不报错 (因为 ClassUtils.isPresent 为 false)。
16.4 验证 exclude 的全局性
java
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class App { }
@Configuration
@EnableAutoConfiguration // 第二个,没有 exclude
class Second { }
期望 :DataSourceAutoConfiguration 仍被排除(Group 的 allExclusions 并集)。
16.5 验证排序
java
@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 的必要性
java
@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 的两种写法
yaml
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
yaml
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:
arduino
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 的实现原理。