Spring Boot 4.1.1 深度源码解析-第 14 章 · 自动配置引擎:`AutoConfigurationImportSelector` 全流程

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 是去中心化的------每个模块自带名单。

这个改动的两个收益:

  1. 候选数量大大减少(不引入 jdbc 就没有 jdbc 的候选),启动更快
  2. 模块自描述,不需要改中心文件

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);
}

逐条规则:

  1. 编码写死 UTF-8(不依赖平台默认编码)✅

  2. 注释用 # ,但注意它是 indexOf("#") ------ 行内注释也支持 :

    bash 复制代码
    com.example.Foo    # 这是一条注释

    会被截断成 com.example.Foo。这一点与 .properties 的 # 规则不同(后者只在行首)。

  3. trim() 后跳过空行

  4. 不做任何去重 ------ 重复的类名会留在列表里,交给 removeDuplicates() 处理(§4.3)

  5. 不做任何排序 ------ 顺序就是 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.");

触发场景:

  1. 用了自定义打包(shade / 自研 Loader),把 META-INF/spring/*.imports 丢了
  2. 用了某些老版本的构建插件,资源过滤把 .imports 排除了
  3. 手写了一个 @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;
}

三个细节:

  1. 默认 true ------ 不配置就是启用
  2. getClass() == AutoConfigurationImportSelector.class 守卫 ------ 只有"正好是这个类的实例"才读开关;子类永远返回 true 。
    • 为什么?子类(ImportAutoConfigurationImportSelector)有自己的启用语义,spring.boot.enableautoconfiguration 不该影响测试切片。
    • 这与第 13 章 TypeExcludeFilter 的 getClass() == TypeExcludeFilter.class 是同一个模式------用精确类比较来区分"是不是我自己"。
  3. 属性名是 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 excludeName attribute.

原因 :当 @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:

  1. ClassUtils.isPresent(exclusion, classLoader) ------ 这个类在 classpath 上
  2. !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(在导入阶段批量预筛)

为什么要两套?

因为性能。正式的条件求值需要:

  1. 加载自动配置类的字节码(ASM 或反射)
  2. 解析注解属性
  3. 构造 ConditionContext
  4. 求值

而预筛只需要读编译期生成的 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);
    }
}

四个要点:

  1. 硬断言只支持 AutoConfigurationImportSelector ------ 因为 Group 需要访问它的 protected 方法(getAutoConfigurationEntry、getAutoConfigurationReplacements)。其他类型的 selector 无法参与这个 Group。

  2. AutoConfigurationReplacements 必须一致 ------ 如果两次 process() 拿到的替换表不同(比如 classpath 上有冲突的 .replacements 文件),直接报错。这是一个一致性校验,避免排序时出现不确定的替换行为。

    AutoConfigurationReplacements 实现了 equals/hashCode(AutoConfigurationReplacements.java),正是为了这个断言。

  3. autoConfigurationEntries 累积所有 entry ------ 每个 process() 一次调用贡献一个。

  4. 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, @AutoConfigureBefore and @AutoConfigureAfter annotations (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 与否都放入 map
  • required = 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。逐步解析:

  1. toSort.remove(0) 取待排序的第一个作为起点(此时已按 order + 字母序排过,第一个就是"最该先排的")
  2. processing.add(current) ------ 标记"正在处理中",用于环检测
  3. classes.getClassesRequestedAfter(current) ------ 见下方语义分析
  4. 对集合里每个元素:递归先处理它
  5. 全部处理完,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;
}

两个来源:

  1. get(className).getAfter() ------ className 自己声明了 @AutoConfigureAfter(X),语义是 X 应排在 className 之前
  2. 遍历所有类,找出声明了 @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 name attribute.

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 @Configuration classes, 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 @DependsOn relationships.

这是一个极其常见的误解:

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:

  1. 解析 App(主配置类)
  2. 遇到 @EnableAutoConfiguration → 立即 selectImports()
  3. 得到 DataSourceAutoConfiguration 等几百个类
  4. 立即递归解析 DataSourceAutoConfiguration
  5. 求值 @ConditionalOnMissingBean(DataSource.class)
  6. 此时容器里只有 12 个 Bean 定义 (主类 + 注解 processor),没有 myDataSource(因为 MyConfig 还没被扫描到!)
  7. → 条件通过 → dataSource() 被注册
  8. 之后扫描发现 MyConfig,注册 myDataSource
  9. 结果:两个 DataSource Bean ❌

用 DeferredImportSelector:

  1. 解析 App
  2. @EnableAutoConfiguration → 记为 deferred,不立即处理
  3. 继续处理 @ComponentScan → 发现 MyConfig → 注册 myDataSource 定义
  4. 继续处理其他 @Import、@Bean
  5. 全部配置类处理完后 → 执行 deferred → selectImports()
  6. 求值 @ConditionalOnMissingBean(DataSource.class) → 此时容器里已有 myDataSource → 条件不通过 → 不注册
  7. 结果:只有用户自己的 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 @EnableAutoConfiguration but restricts the auto-configuration classes to the specified set, rather than consulting ImportCandidates.

即 :不走 .imports 名单,只导入你指定的那几个。

主要用途(Javadoc 明说):

@ImportAutoConfiguration can 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]
  • 结果:[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. 本章未逐行核实 / 待你实测的项

  1. AutoConfigurationImportSelector 在不同上下文类型下的差异未逐个验证。
  2. AutoConfigurationSorter 三轮排序的实现细节在第 16 章补充,本章只给结论。
  3. AutoConfigurationMetadata 的生成与消费链路未逐行读。
  4. @AutoConfiguration 的 before / after 冲突检测规则未穷举。
  5. 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 的实现原理。

相关推荐
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 24 章 · 日志系统:`LoggingApplicationListener` 与结构化日志
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 22 章 · 数据源、连接池与事务
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 17 章 应用类型推断与上下文工厂:`WebApplicationType`
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 23 章 · JPA / Hibernate 与持久化模块
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 12 章 · 事件总线与 Bootstrap 上下文
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 05 章 · `ApplicationProperties` 与 `spring.main.*`:启动配置
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 07 章 · ConfigData 体系深潜:位置解析、profile 迭代求解
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 20 章 · WebFlux 与 Reactor Netty:响应式栈与编解码
java·后端·开源
鶴哥只手遮天1 小时前
Spring Boot 4.1.1 深度源码解析-第 21 章 · gRPC 新纪元:`spring-boot-grpc-server`
java·后端·开源