Spring Boot 自动配置:条件注解、AutoConfiguration.imports 和加载顺序(096)

Spring与Spring Boot约 24 分钟阅读13,400 字符
知识库文档 97 / 251

引言:为什么"零配置"不等于"零理解"

Spring Boot 最吸引人的卖点之一就是"开箱即用"——引入一个 Starter,什么都不用写,就能得到一个功能完备的 DataSource、RedisTemplate 或 RabbitTemplate。但如果面试只停留在"我知道怎么用",这道题已经输了。面试官真正想考察的是:当自动配置不符合预期时(比如引入了一个自定义 DataSource,但 Spring Boot 的默认 DataSource 自动配置也同时生效了,导致冲突),你能不能在几分钟内定位到问题根因,而不是"把依赖删了重试一遍"。这篇文章从 @Conditional 注解的底层判断逻辑,到 AutoConfiguration.imports 文件的加载机制,再到自动配置类之间的顺序控制,把"自动配置为什么能自动"这件事彻底拆开。

一、自动配置的入口:@EnableAutoConfiguration 与 AUTO_CONFIGURATION_ATTRIBUTE

文章配图
文章配图

文章配图
文章配图

Spring Boot 的启动流程里,自动配置是怎么被触发的

自动配置的入口是 @EnableAutoConfiguration 注解,它通常被 @SpringBootApplication 组合注解间接引入:

java
@SpringBootApplication  // 等价于 @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
public class MyApplication {

    public static void main(String[ ] args) {

        SpringApplication.run(MyApplication.class, args);
    }
}

@EnableAutoConfiguration 的核心是 @Import(AutoConfigurationImportSelector.class)。Spring 在处理 @Import 时会调用 AutoConfigurationImportSelector 的 selectImports 方法,返回一组自动配置类的全限定名,容器然后把这些类当作"普通 @Configuration 类"一样注册进 Bean 定义。

关键代码路径(AutoConfigurationImportSelector):

java
// 简化的核心逻辑
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 从 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 读取
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
        EnableAutoConfiguration.class, getBeanClassLoader());
    return configurations;
}

到这一步先记住两个关键信息:第一,自动配置类本质上就是普通的 @Configuration 类,不是什么特殊的"先于 Bean 加载"的预处理机制;第二,自动配置类列表是从 AutoConfiguration.imports 文件里读出来的——这是理解下一节的基础。

自动配置类里为什么写满了 @Conditional 注解

把自动配置类当普通 @Configuration 类注册有一个问题:如果把所有自动配置类都无脑注册,等于强行创建了 DataSource、RedisTemplate、RabbitTemplate 等所有 Bean,不管应用实际有没有引入对应的依赖。这显然不行。

解决方式就是条件注解:每个自动配置类(或内部的 @Bean 方法)都标注了 @ConditionalOnXxx 注解,Spring 在注册 Bean 定义之前会先评估这些条件。条件不满足时,整个配置类(或单个 @Bean 方法)被跳过,不注册任何 Bean 定义——这就是"自动"的本质:不是不配置,而是根据当前环境条件自动决定哪些配置生效。

二、条件注解体系:自动配置的"决策引擎"

文章配图
文章配图

条件注解全景

Spring Boot 提供了 30+ 个条件注解,但最核心的集中在以下七类:

注解 判断依据 典型用法
@ConditionalOnClass 指定类是否在 classpath 只有引入特定依赖才启用自动配置
@ConditionalOnMissingClass 指定类是否不在 classpath 同时存在多个候选实现时做互斥
@ConditionalOnBean 指定类型 Bean 是否已存在 只有用户自定义了某个 Bean 时才添加增强
@ConditionalOnMissingBean 指定类型 Bean 是否不存在 用户没有自定义时才使用默认实现
@ConditionalOnProperty 指定配置项的值 通过配置开关控制功能启用
@ConditionalOnResource 指定资源文件是否存在 只有特定配置文件存在时才启用
@ConditionalOnWebApplication 是否为 Web 环境 区分 Web 与非 Web 场景

@ConditionalOnClass:最常见也最容易出问题的条件

@ConditionalOnClass 判断的是类在 classpath 中是否存在,它不关心这个类是否被加载过、是否被 Spring 管理,只看 ClassLoader 能不能找到它。这是自动配置体系中最重要的条件注解——绝大部分"引入 Starter 自动配置就生效"的魔法,底层都是它。

java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
public class RedisAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory factory) {
        // 默认 RedisTemplate 配置
    }
}

注意 @ConditionalOnClass 的参数 value 和 name 的区别:value 是 Class<?> 类型,编译期要能解析到具体的类;name 是 String 类型,运行时才去加载。如果某个自动配置依赖的类你不想在编译期硬依赖(比如一个可选的三方库),用 name = "com.example.SomeClass" 是更灵活的选择。但工程上 name 参数里的字符串在 IDE 重构时不会跟着改名,容易引入隐蔽的 bug——在自研 Starter 里建议优先用 value。

@ConditionalOnBean / @ConditionalOnMissingBean:Bean 存在性判断的陷阱

这两个注解的语义很直观:判断某个类型(或多个类型)的 Bean 是否已经存在于容器中。但这里有三个容易被忽视的细节:

第一,判断时机。@ConditionalOnBean 的评估发生在 Bean 定义注册阶段,而不是 Bean 实例化之后。这意味着它只能判断"Bean 定义是否已经注册",不能判断"Bean 是否已经成功初始化"。如果用户定义的 Bean 在初始化过程中失败了,@ConditionalOnBean 不会感知到——它仍然会认为 Bean 存在,继续注册自动配置的 Bean。这可能导致后续的 Bean 初始化也因为缺少依赖而失败,但错误信息往往指向的是"为什么这个自动配置的 Bean 也注册了",而不是"为什么用户自定义的 Bean 初始化失败了"——增加排查成本。

第二,搜索范围。@ConditionalOnBean 默认只在当前 ApplicationContext 的 Bean 定义中搜索。如果应用中有父子容器(比如 Spring MVC 的 DispatcherServlet 子容器),子容器里的自动配置类用 @ConditionalOnBean 查找父容器里注册的 Bean 是找不到的——除非把 search 参数设为 SearchStrategy.ANCESTORS。

第三,顺序依赖。 自动配置类之间的加载顺序由 @AutoConfigureBefore / @AutoConfigureAfter 控制(后面会展开),但 @ConditionalOnBean 的判断结果也受这个顺序影响——如果 A 自动配置类在 B 之前加载,B 通过 @ConditionalOnBean 判断 A 是否注册了某个 Bean 时,A 的 Bean 定义已经存在,判断为真;反过来如果 B 先于 A 加载,同一个判断就会为假。这种"顺序敏感的 Bean 存在性判断"是自动配置调试中最难以追踪的问题之一。

@ConditionalOnProperty:配置驱动的特性开关

@ConditionalOnProperty 允许通过 application.properties 或 application.yml 中的配置项来控制某个自动配置是否生效:

java
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.cache", name = "type", havingValue = "redis")
public class RedisCacheConfiguration { ... }

它的完整参数包括:

参数 说明
prefix 配置项前缀,拼接后形成完整 key
name / value 配置项名称(可多个,用 AND 逻辑)
havingValue 期望的值,默认 "true"
matchIfMissing 配置项不存在时是否认为匹配,默认 false

matchIfMissing 是一个容易被用错的参数。它的语义是:如果这个配置项在 application.properties 里完全不存在,是否认为条件满足。默认 false 意味着"不配置就不启用",这是安全默认值。但如果某个自动配置类的默认行为是"启用"(比如 DataSourceAutoConfiguration),matchIfMissing 就应该是 true——否则用户不写配置项,自动配置就不生效了,这跟"约定优于配置"的理念背道而驰。

@ConditionalOnClass 的底层实现使用了 ASM 而非反射

这是面试中一个经常被追问的细节:@ConditionalOnClass 判断类是否存在时,用的是 ASM 字节码读取,而不是 Class.forName() 反射。为什么?

因为自动配置类本身在条件评估时,可能依赖的类并不在 classpath 上——如果直接用 Class.forName("com.example.SomeMissingClass") 会抛出 ClassNotFoundException,而 @ConditionalOnClass 的语义恰好是"这个类不存在时跳过"——用反射就变成了"判断类不存在"这个操作本身先抛了异常,条件评估失败。

Spring 的做法是:OnClassCondition 内部使用 ASM 的 ClassReader 直接读取字节码,解析类文件中的常量池和注解信息,提取出需要的类名,然后通过 ClassLoader.getResource() 把类名转换为文件路径判断文件是否存在——全程不触发任何类的加载。这个设计也意味着:@ConditionalOnClass 只能判断一个类是否在 classpath 中,不能判断这个类是否真的是一个可用的、能正常加载的类——如果一个 jar 包存在但类文件损坏,@ConditionalOnClass 仍然会认为条件满足,直到后续 Spring 真正去加载这个类时才会报错。

三、AutoConfiguration.imports:从 spring.factories 到专用文件的演进

文章配图
文章配图

Spring Boot 2.7 之前:spring.factories

在 Spring Boot 2.7 之前,自动配置类列表写在 META-INF/spring.factories 文件中:

properties
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration,\
com.example.AnotherAutoConfiguration

这个文件其实是 Spring 的 SpringFactoriesLoader 机制的一个通用文件,它不光用于自动配置,还用于注册 ApplicationListener、EnvironmentPostProcessor、FailureAnalyzer 等各种 SPI 扩展——所有扩展点都混在一个文件里。随着自动配置类数量增长(Spring Boot 官方的 auto-configuration 列表超过了 180 个类),这个文件的单行变得越来越长,合并冲突也越来越频繁,更关键的是:自动配置类的顺序信息(**@AutoConfigureBefore**/**@AutoConfigureAfter**)跟自动配置类列表本身不在同一个地方——列表在 spring.factories,顺序在注解上,分离了本应放在一起决策的信息。

Spring Boot 2.7 之后:专用文件 AutoConfiguration.imports

从 Spring Boot 2.7 开始,引入了专用的 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件:

code
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration
com.example.AnotherAutoConfiguration

每行一个类名,不再需要反斜杠续行,合并冲突的粒度从"一行 180 个类名"降到了"一行一个类名"。同时,这个文件只用于自动配置类的注册,不再跟其他 SPI 扩展混在一起。

SpringFactoriesLoader 的加载逻辑也做了适配:loadFactoryNames 方法会优先读取 AutoConfiguration.imports 文件,如果不存在则回退到 spring.factories。这意味着兼容性没有问题——只要你的自动配置类仍然写在 spring.factories 里,升级 Spring Boot 版本后不会立即失效。

旧版 spring.factories 的另一个坑:多 jar 覆盖

spring.factories 文件的加载逻辑是:扫描 classpath 中所有 jar 包里的同名文件,把所有内容合并。这意味着如果一个 jar 包里的 spring.factories 文件格式有误(比如最后一行缺少反斜杠),会导致整个合并后的列表截断,后面所有 jar 包的自动配置类全部丢失——而且没有任何明显的报错,只是"部分自动配置不生效"。AutoConfiguration.imports 每行独立,不存在这种"一个 jar 写错影响全局"的问题。

四、自动配置类的加载顺序:谁先谁后

文章配图
文章配图

文章配图
文章配图

为什么顺序很重要

自动配置类不是孤立的——它们之间有依赖关系。比如 DataSourceAutoConfiguration 需要先注册 DataSource Bean,然后 JdbcTemplateAutoConfiguration 才能基于已有的 DataSource 创建 JdbcTemplate。如果顺序反了,JdbcTemplateAutoConfiguration 里的 @ConditionalOnBean(DataSource.class) 会判断为"不存在"(因为 DataSourceAutoConfiguration 还没加载),导致 JdbcTemplate 没有被创建——但用户看到的是 DataSource 确实存在,JdbcTemplate 却莫名其妙没有,排查起来非常困惑。

@AutoConfigureBefore / @AutoConfigureAfter

Spring Boot 提供了两个注解来声明自动配置类之间的顺序依赖:

java
@AutoConfiguration
@AutoConfigureAfter(DataSourceAutoConfiguration.class)
public class JdbcTemplateAutoConfiguration {
    @Bean
    @ConditionalOnBean(DataSource.class)
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

这两个注解的语义完全对称:@AutoConfigureAfter 表示"当前类应该在指定类之后加载",@AutoConfigureBefore 表示"当前类应该在指定类之前加载"。它们是声明式的——只是"建议"一个顺序,不强制。如果声明的顺序和实际依赖关系冲突(比如 A 声明在 B 之后,B 又声明在 A 之后),Spring 不会报错,而是按默认的字典序做解析——这种"不报错只静默排序"的行为在排查时非常隐蔽。

@AutoConfigureOrder

@AutoConfigureOrder 跟 @AutoConfigureBefore / @AutoConfigureAfter 解决的是不同层次的问题:前者是基于"绝对顺序值"的排序,后者是基于"相对关系"的排序。逻辑上,@AutoConfigureBefore / @AutoConfigureAfter 的优先级更高——先解析相对关系构建拓扑排序,再在拓扑排序的每个层级内按 @AutoConfigureOrder 的值排序。

实际上,Spring Boot 官方的自动配置类几乎全部依赖 @AutoConfigureBefore / @AutoConfigureAfter 来控制顺序,@AutoConfigureOrder 的使用场景很少——大多数自动配置类之间的顺序关系是"A 必须在 B 之前",而不是"A 应该在所有自动配置类中排第 3 位"。只有当你确实需要控制一个自动配置类在全局的加载位置(而不是相对于某个特定类的位置)时,才需要 @AutoConfigureOrder。

自动配置类内部 @Bean 方法的顺序

自动配置类加载之后,内部的 @Bean 方法之间也有顺序依赖。这个顺序不受 @AutoConfigureBefore / @AutoConfigureAfter 影响——它受 @DependsOn 控制。如果同一个自动配置类内部多个 @Bean 方法之间有"B 依赖 A"的关系,可以用 @DependsOn 或直接在方法参数中声明依赖(Spring 会自动推断顺序),参见前面的 JdbcTemplate 例子——jdbcTemplate(DataSource dataSource) 方法参数中的 DataSource 就是一种隐式的顺序依赖声明。

五、自动配置的排除机制:当默认行为不符合预期时

文章配图
文章配图

三种排除方式

如果你不想让某个自动配置类生效,有三种方式:

方式一:**@EnableAutoConfiguration**的**exclude**属性

java
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class MyApplication { ... }

方式二:**spring.autoconfigure.exclude**配置项

properties
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

方式三:**@ConditionalOnProperty**的 matchIfMissing 兜底

如果自动配置类本身提供了 @ConditionalOnProperty 开关,关闭对应的配置即可:

properties
spring.autoconfigure.data.redis.enabled=false  # 不是每个自动配置都有这个开关

排除机制的底层实现

不管是方式一还是方式二,最终都会在 AutoConfigurationImportSelector 里被处理:exclude 属性指定的类名以及 spring.autoconfigure.exclude 配置项指定的类名,都会被收集到一个排除集合中,在加载自动配置类列表时过滤掉。排除发生在条件评估之前——被排除的自动配置类根本不会进入条件评估流程,所以即使条件满足也不会生效。这是排查"为什么自动配置不生效"时的一个关键区分点:是"被排除了"还是"条件不满足"。

排除和条件注解的区别

很多人容易混淆"排除"和"条件不满足":排除是用户主动声明"我不想要这个自动配置",条件不满足是框架自动判断"当前环境不需要这个自动配置"。从排查角度,如果你发现某个自动配置没生效,先检查 spring.autoconfigure.exclude 和 @EnableAutoConfiguration 的 exclude 属性——如果不在排除列表里,再去看条件注解的评估结果。

六、条件注解底层实现原理

文章配图
文章配图

ConditionEvaluator 的评估流程

条件注解的评估由 ConditionEvaluator 完成,核心方法是 shouldSkip:

java
// 简化流程
public boolean shouldSkip(AnnotatedTypeMetadata metadata, ConditionContext context) {

    for (String[ ] conditionClasses : getConditionClasses(metadata)) {

        for (String conditionClass : conditionClasses) {
            Condition condition = getCondition(conditionClass, context);
            if (!condition.matches(context, metadata)) {
                return true;  // 任一条件不满足,跳过
            }
        }
    }
    return false;  // 所有条件都满足,不跳过
}

关键点:多个条件注解是 AND 关系——一个自动配置类上标注了 @ConditionalOnClass 和 @ConditionalOnProperty,两个条件都满足才算通过。同级条件注解(比如 @ConditionalOnBean 的 value 参数里指定多个类)也是 AND 关系。如果需要在条件之间做 OR 逻辑,需要自定义 Condition 接口实现。

OnClassCondition 的 ASM 优化

前面提到 OnClassCondition 使用 ASM 而非反射来避免加载类。它的具体实现是在 AutoConfigurationImportSelector 被调用时,先收集所有 @ConditionalOnClass 注解中指定的类名,一次性批量检查——而不是每个自动配置类单独检查一次。这个批量检查有缓存机制:同一个类名在全量自动配置类中可能被多次引用(比如 DataSource 类名被多个自动配置类用到),只检查一次。

条件评估报告:debug=true 背后的故事

在 application.properties 中设置 debug=true,Spring Boot 会在启动日志中输出一份"条件评估报告"(Auto-configuration Report),包括:

  • Positive matches:条件满足、自动配置生效的类及其匹配的条件注解
  • Negative matches:条件不满足、自动配置被跳过的类及其未满足的条件注解
  • Exclusions:被显式排除的自动配置类
  • Unconditional classes:没有条件注解、始终生效的自动配置类

这份报告由 ConditionEvaluationReport 类在评估过程中逐步构建,本质上是一个 Map<String, ConditionAndOutcome>,key 是自动配置类全名,value 是条件及其匹配结果。debug 模式下的日志输出只是这个数据结构的格式化展示——如果你需要在自己的监控系统里查看自动配置加载情况,可以直接注入 ConditionEvaluationReport 把结果上报到你的可观测性平台。

七、自定义 Starter 实战:从注解到可插拔模块

文章配图
文章配图

命名规范

Spring Boot 官方 Starter 的命名规范是 spring-boot-starter-{模块名},第三方 Starter 建议用 {模块名}-spring-boot-starter。这不是硬性要求,但遵循这个约定能让使用者在看到依赖名时立刻区分"这是官方的还是第三方的"。

典型的 Starter 项目结构

code
my-starter/
├── pom.xml
├── src/main/java/
│   └── com/example/starter/
│       ├── MyAutoConfiguration.java          # 自动配置类
│       ├── MyProperties.java                 # 配置属性类
│       └── MyService.java                    # 提供的核心功能
└── src/main/resources/
    └── META-INF/spring/
        └── org.springframework.boot.autoconfigure.AutoConfiguration.imports

自动配置类

java
@AutoConfiguration  // 等价于 @Configuration,Spring Boot 2.7+ 推荐
@EnableConfigurationProperties(MyProperties.class)  // 将 @ConfigurationProperties 类注册为 Bean
@ConditionalOnClass(MyService.class)  // 只在 classpath 有此依赖时生效
@ConditionalOnProperty(prefix = "my.starter", name = "enabled", havingValue = "true", matchIfMissing = true)
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean  // 用户没有自定义时才注册
    public MyService myService(MyProperties properties) {
        return new MyService(properties);
    }
}

配置属性类

java
@ConfigurationProperties(prefix = "my.starter")
public class MyProperties {
    private boolean enabled = true;
    private String url;
    private int timeout = 5000;
    // getters, setters
}

AutoConfiguration.imports 注册

code
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.starter.MyAutoConfiguration

一个关键的工程实践:避免在 Starter 里做组件扫描

自定义 Starter 在 @AutoConfiguration 类上不应该加 @ComponentScan——因为 @ComponentScan 会扫描 Starter 所在包路径下的所有 Spring 注解类,这不仅慢,而且会意外注册一些本不该注册的 Bean(比如 Starter 内部的工具类)。正确的做法是:只在 @AutoConfiguration 类中通过 @Bean 方法显式注册需要暴露的 Bean,让 Bean 的注册边界完全可控。

八、常见陷阱与调试技巧

文章配图
文章配图

文章配图
文章配图

陷阱一:自动配置类被排除但用户不知道

场景:排查"为什么 DataSource 自动配置没生效",检查了 spring.datasource 配置项没问题,检查了 classpath 上有 HikariCP,条件报告也显示条件满足——但最终 DataSource 就是没注册。排查了半小时才发现项目的 application-{profile}.yml 里有一行 spring.autoconfigure.exclude 把 DataSourceAutoConfiguration 排除了,而这个配置是半年前某次紧急修复加上去的,没人记得了。

排查方法:先看 debug=true 条件评估报告里的 Exclusions 部分,确认没有被排除,再去看条件匹配结果。

陷阱二:@ConditionalOnBean 搜索不到 Bean

场景:在父容器里注册了 DataSource,在子容器(DispatcherServlet)里有一个自动配置类用 @ConditionalOnBean(DataSource.class) 判断——结果为 false,因为默认只在当前容器搜索。解决办法是加 search = SearchStrategy.ANCESTORS。

陷阱三:@ConditionalOnClass 用了 value 参数导致编译期依赖

场景:自定义 Starter 里用 @ConditionalOnClass(SomeOptionalClass.class) 判断某个可选依赖,但 SomeOptionalClass 在 Starter 的 compile scope 里不存在,编译失败。解决办法是用 name 参数替代 value 参数,或者把可选依赖的 scope 设为 provided(只在编译期存在,运行时由使用方引入)。

调试开关:debug=true 和自定义条件报告

前面提到的 debug=true 会输出全量条件评估报告,但生产环境不建议全局开启——日志量太大。更好的做法是在你的可观测性平台上,通过 ConditionEvaluationReport Bean 在应用启动后抓取一次自动配置的匹配情况,上报到监控系统,用于排查"为什么某个自动配置在某个环境没生效"的问题。

启动时自动配置类加载耗时排查

当一个项目依赖了大量 Starter 时,自动配置类的加载和条件评估可能成为启动耗时的一部分。Spring Boot 3.x 提供了 AOT(Ahead-of-Time)处理机制,可以在编译期完成条件评估,把结果直接写入代码,运行时跳过条件评估——但这需要配合 Spring Boot 3.x + GraalVM Native Image 使用,普通的 JVM 部署模式目前仍然需要运行时评估。

小结:从"自动"到"可控"

理解自动配置的完整链路之后,回头看"开箱即用"这四个字,它不再是魔法——它是:

  1. AutoConfigurationImportSelector 从 AutoConfiguration.imports 文件加载所有自动配置类的全限定名
  2. 每个自动配置类上标注了条件注解,ConditionEvaluator 逐个评估,条件不满足的跳过
  3. 通过 @AutoConfigureBefore / @AutoConfigureAfter 控制自动配置类之间的加载顺序
  4. 用户可以显式排除不需要的自动配置类,或者通过自定义同名 Bean(借助 @ConditionalOnMissingBean)覆盖默认实现

把这条链路理解透,你不会再遇到"自动配置怎么不生效了"这种问题时束手无策——你知道从哪个环节开始排查,知道每个环节的输入输出是什么,知道报告里"Positive match"和"Negative match"各自意味着什么。

(以上正文阅读时长约 40-50 分钟,以下问答部分不计入正文阅读时长)

文末答疑:面试官会怎么问

问 1:Spring Boot 官方说"自动配置在用户定义的 Bean 之后加载",这是一条硬规则吗?如果是,它是怎么实现的?

答:这不是一条硬规则,而是"大多数情况下成立"的结果。它由两个机制叠加实现:第一,AutoConfigurationImportSelector 在容器刷新(refresh)的 invokeBeanFactoryPostProcessors 阶段执行,而用户自定义的 @Configuration 类通常通过 @ComponentScan 更早注册;第二,自动配置类上大量使用了 @ConditionalOnMissingBean,即使自动配置类先加载,只要用户已经定义了同名 Bean,自动配置的 Bean 就不会注册。但严格来说,加载顺序不等于最终生效顺序——一个自动配置类可能因为 @AutoConfigureBefore 声明而比某些用户自定义的 @Configuration 类更早加载,只是它注册的 Bean 被 @ConditionalOnMissingBean 拦截了而已。

问 2:你刚才提到**@ConditionalOnClass**用 ASM 而不是反射来检查类是否存在。如果我用自定义 ClassLoader 动态加载类,**@ConditionalOnClass**的判断结果会受到影响吗?

答:会。OnClassCondition 内部通过 ClassLoader.getResource() 把类名转换为文件路径(比如 com.example.Foo → com/example/Foo.class),然后检查这个资源在 classpath 中是否存在。如果自定义 ClassLoader 的类加载逻辑不遵循"从 classpath 文件系统读取"的标准路径映射(比如从网络流、动态字节码生成、加密 jar 包中加载),getResource() 可能返回 null,导致 @ConditionalOnClass 误判为"类不存在"——即使实际上这个 ClassLoader 能成功加载这个类。这是 @ConditionalOnClass 设计上的一个隐含假设:类在 classpath 上的"物理存在"和"能通过 ClassLoader 加载"是等价的。在绝大多数场景下这个假设成立,但在自定义 ClassLoader 场景下会被打破。

问 3:从 Spring Boot 2.7 开始,**spring.factories**被**AutoConfiguration.imports**取代。如果你的项目需要同时兼容 Spring Boot 2.6 和 2.7+,自动配置类应该写在哪个文件里?

答:两个文件都写。Spring Boot 2.7+ 的 SpringFactoriesLoader 会优先读取 AutoConfiguration.imports,如果文件不存在则回退到 spring.factories——这意味着:在 AutoConfiguration.imports 写入自动配置类列表,同时在 spring.factories 里也保留一份(用于 2.6 及以下版本),2.7+ 会从 AutoConfiguration.imports 读取(忽略 spring.factories 里的同名条目),2.6 及以下版本找不到 AutoConfiguration.imports 文件,从 spring.factories 读取。唯一的代价是两处维护——但考虑到这是过渡期,可以接受。

问 4:**@ConditionalOnBean**和**@DependsOn**都表达"依赖某个 Bean",它们有什么区别?应该分别在什么场景使用?

答:这是两个完全不同层面的概念。@ConditionalOnBean 是条件注解,它决定的是"这个配置类/Bean 定义要不要注册"——如果依赖的 Bean 不存在,当前 Bean 根本不会被注册,容器里不会留下它的任何痕迹。@DependsOn 是初始化顺序注解,它假设两个 Bean 都已注册,只是控制谁先初始化——如果 @DependsOn 指定的 Bean 不存在,初始化会直接失败。类比:@ConditionalOnBean 是"如果 A 没来,我就不进教室",@DependsOn 是"我和 A 都进了教室,但我保证 A 先拿书再轮到我"。在自动配置场景中,大多数情况下应该用 @ConditionalOnBean(因为用户可能没有引入对应的依赖),只有在同一个自动配置类内部、两个都必须注册的 Bean 之间,才用 @DependsOn 或参数注入来控制初始化顺序。

问 5:如果一个自动配置类同时标注了**@ConditionalOnClass**和**@ConditionalOnMissingBean**,但 classpath 上同时存在两个实现了同一个接口的 jar 包,**@ConditionalOnMissingBean**的判断结果会是什么?

答:这取决于两个 jar 包的自动配置类加载顺序。@ConditionalOnMissingBean 判断的是"当前容器中是否已经存在这个类型的 Bean 定义"——如果先加载的自动配置类注册了 Bean,后加载的自动配置类里的 @ConditionalOnMissingBean 就会判断为"已存在",跳过注册。但哪个自动配置类先加载,由 @AutoConfigureBefore / @AutoConfigureAfter 和 @AutoConfigureOrder 决定。如果两个自动配置类都没有声明顺序关系,加载顺序是不确定的(取决于 classpath 扫描顺序),这种情况下 @ConditionalOnMissingBean 的结果也是不确定的——今天在 A 环境生效的是 jar1 的 Bean,明天换到 B 环境可能就成了 jar2 的 Bean。这是一个工程上需要特别注意的边界情况:当你的系统里存在多个候选自动配置实现同一个接口时,必须用 @AutoConfigureBefore 或 @AutoConfigureAfter 显式声明顺序,或者在 @ConditionalOnMissingBean 的基础上加 @ConditionalOnClass 做更细粒度的互斥。