刚接手一个从SSM升级上来的项目时,我盯着空空的启动类沉默了十几秒。那种感觉就像搬家后发现物业把家具都配好了,虽然省事,但总怕哪里藏着问题。后来把Spring Boot自动配置源码啃下来,才真正理解:它不是在XML外面包了一层注解,而是把“谁负责装配”这件事整个换了个玩法。这篇文章会把自动配置从入口注解、候选清单、条件裁决到装配流程完整拆开,最后聊自定义Starter和排查经验,适合所有被自动配置“黑箱”困扰过的Java开发者。
1. 一个老Java开发者的疑问:几十个XML到底被谁替代了
1.1 回忆XML时代的一个典型装配现场
我最早做项目时,Spring还是纯XML配置。一个稍微正经点的web应用,applicationContext.xml里要塞下数据源、事务管理器、MyBatis的SqlSessionFactory、MapperScannerConfigurer、视图解析器、拦截器、消息转换器……几百行Bean定义非常常见。每次新同事入职,光理解那些Bean之间的依赖就要花掉两三天。
当时的配置长这样:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/mall"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mall.mapper"/> </bean>现在再看这些内容,多少人会心头一紧。Spring Boot时代,只要引入spring-boot-starter-jdbc和mybatis-spring-boot-starter,在application.yml里写几行spring.datasource.*就完事了。很多基于Spring Boot加MyBatis的开源多商户跨境商城项目,也是靠自动配置把数据源、MyBatis、连接池这些组件串起来的。
1.2 自动配置的本质:装配责任的移交
XML被取代这件事,深层原因不是“注解比XML更简洁”,而是整个装配逻辑的决策权换了位置。
XML时代,是“用户告诉Spring容器:我要这几个Bean,我给你它们的类型、依赖、参数”。框架只是个执行者,装配规则掌握在开发者手里。
自动配置时代,变成“用户告诉Spring Boot:我引入了哪些依赖”。框架自己扫描classpath,推断出“哦,有HikariCP、有MySQL驱动、有MyBatis,那你八成需要数据源和SqlSessionFactory”,然后按既定规则把这些Bean装配好。
这其实不是一个技术细节的变化,而是开发范式的变化:装配责任从用户移交给了框架的推断机制。
1.3 为什么理解原理仍然必要
有人可能会有疑问:既然自动配置这么好用,我直接躺平不就得了?问题在于,任何“推断”都可能猜错。
- 你项目里引入了两个数据源依赖,它到底用哪个?
- 你自己定义了一个
RestTemplateBean,为什么后来又悄悄多出来一个? - 把项目从Spring Boot 2.x升到3.x,为什么自研组件的自动配置突然失效?
这些问题,不懂底层机制根本没法排查。我见过不少同事遇到自动配置相关的问题,只能靠“百度+试配置项”碰运气,运气不好一上午就没了。把原理吃透,至少你拿到一个CONDITIONS EVALUATION REPORT时,能一眼看懂它到底在说什么。
2. 启动类上的三个注解组合,拆开全是信息
2.1 @SpringBootApplication 的秘密:不只是个组合注解
大多数Spring Boot项目长这样:
@SpringBootApplication public class MallApplication { public static void main(String[] args) { SpringApplication.run(MallApplication.class, args); } }@SpringBootApplication不是新发明,它是三个注解的组合:
| 注解 | 职责 |
|---|---|
| @SpringBootConfiguration | 本质上是个 @Configuration,标记当前类是主配置类 |
| @EnableAutoConfiguration | 开启自动配置,向容器中导入大量候选配置类 |
| @ComponentScan | 扫描当前类所在包及其子包下的 @Component、@Service 等组件 |
完整的@SpringBootApplication定义长这样(Spring Boot 3.x版本):
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { }注意看@ComponentScan上挂了两个排除过滤器。这里有一个很多人没注意到的细节:AutoConfigurationExcludeFilter会把“已经被自动配置机制导入的类”从组件扫描结果里排除掉。如果不做这一步,你把自动配置类放在启动类同一个包下时,它会被@ComponentScan和@EnableAutoConfiguration各扫一次,造成双重注册。
2.2 @EnableAutoConfiguration 到底导入了个啥
再往下挖一层。@EnableAutoConfiguration的定义:
@AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { }核心是@Import(AutoConfigurationImportSelector.class)。AutoConfigurationImportSelector是一个DeferredImportSelector,它的selectImports方法会返回一串类名,这些类名就是要尝试装配的候选配置类。
这里有个重要的设计:它叫“Deferred”(延迟)ImportSelector。也就是说,自动配置类的导入时机被推迟了——它会在所有用户自己的@Configuration、@ComponentScan等处理完之后再执行。这样做的目的是让自动配置类能先看到容器里已有的用户Bean定义,从而决定“用户都配了,我就不动了”。
@AutoConfigurationPackage则是把启动类所在的包注册为“自动配置包的根包”。后续JPA的实体扫描、MyBatis的Mapper扫描等,很多都会默认从这些根包开始找,这也是为什么官方建议把启动类放在最外层包。
2.3 自动配置与@ComponentScan的边界
把两个注解拆开看,你就会明白自动配置和普通组件扫描的分工:
@ComponentScan处理的是你自己写的业务类,比如@Controller、@Service、@Repository、@Component。@EnableAutoConfiguration处理的是框架层面的通用装配,比如数据源、事务管理器、Redis客户端、消息转换器。
自动配置类虽然是“配置类”,但它不是你的业务代码,它来自第三方jar包,放在依赖库的META-INF目录下。所以Spring Boot必须单独设计一套加载机制,专门发现这些“藏在jar包里的配置类”。这就是下一章要讲的候选清单机制。
3. 自动配置候选清单的演进:从spring.factories到AutoConfiguration.imports
3.1 旧时代的清单:META-INF/spring.factories
自动配置要加载大量jar包里的配置类,那框架是怎么知道有哪些类可以加载的?答案在一个约定俗成的文件里。
Spring Boot 2.x时代(以及更早),所有自动配置类都要在jar包里的META-INF/spring.factories文件中登记:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.mycommon.TpAutoConfiguration,\ com.example.mycommon.LogAutoConfigurationAutoConfigurationImportSelector会通过SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader)读取所有jar包里的这个key,把后面跟的类名全部收集起来,作为自动配置的候选列表。
3.2 新时代的清单:AutoConfiguration.imports
从Spring Boot 2.7开始,官方引入了一个新文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。格式也变了,不再是properties风格,而是每行一个类名:
com.example.mycommon.TpAutoConfiguration com.example.mycommon.LogAutoConfiguration2.7版本是双轨制,两个文件都能用。但从Spring Boot 3.0开始,自动配置不再读取spring.factories里的EnableAutoConfigurationkey。如果你还在用老方式注册自动配置类,升到3.x之后会直接失效,而且没有任何提示。
这里要澄清一点:“不再读取spring.factories”特指自动配置这一栏。Spring Boot自身还有许多扩展点,比如EnvironmentPostProcessor、ApplicationContextInitializer、ApplicationListener这些仍然走spring.factories机制。所以你不能笼统地说“Spring Boot 3.0淘汰了spring.factories文件”,准确的说法是“自动配置清单不再使用它”。
| 维度 | spring.factories | AutoConfiguration.imports |
|---|---|---|
| 文件路径 | META-INF/spring.factories | META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports |
| 格式 | properties键值对 | 每行一个全限定类名 |
| 定位 | 通用扩展点,不止自动配置 | 专属于自动配置 |
| 2.x支持情况 | 支持 | 2.7开始支持 |
| 3.0支持情况 | 自动配置不支持 | 唯一推荐方式 |
3.3 官方为什么要费劲换掉spring.factories
关于这次替换,我的理解是这样的。
第一,spring.factories是个“大杂烩”。同一个文件里可能有EnableAutoConfiguration、ApplicationListener、EnvironmentPostProcessor、FailureAnalyzer等一大堆key。每次读取自动配置时,框架都得把整个文件解析一遍,再按key筛选,效率不高,职责也不清晰。
第二,自动配置的安全要求更高。自动配置的候选类最终要被注入容器,如果jar包作者往spring.factories里写了一个有恶意行为的类名,它就会在项目启动时被加载。imports文件单独拎出来,可以让自动配置清单的加载路径更干净,也方便框架对这部分做专门的校验和理解。
第三,新文件配合了新的注解体系。Spring Boot 2.7起推荐用@AutoConfiguration注解代替@Configuration加@AutoConfigureBefore/@AutoConfigureAfter的组合。这个注解明确表达了“我是一个自动配置类”的语义,而imports文件本身就是为这种语义服务的。
不过,拿到候选清单只是第一步。Spring Boot内置的自动配置候选类有一百多个,如果把每个类都完整解析、注册,启动速度会变得非常感人。所以框架在真正解析之前还有一道优化流程:先做条件预过滤,再排序去重。
这个流程大概是:
- 读取所有jar包的imports文件,得到候选类名列表。
- 用
AutoConfigurationImportFilter做一次“粗筛”,这一阶段主要用OnClassCondition去判断候选类上的@ConditionalOnClass是否满足,不满足的直接剔除。 - 用
AutoConfigurationSorter对候选类排序,处理@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder这些顺序关系。 - 去重后交给
ConfigurationClassParser继续解析,解析时再对配置类内部的方法级条件做精细判断。
这个粗筛阶段很关键。它用ASM读取类的元数据,而不是真的用Class.forName加载类,所以能避免不满足条件的自动配置类把静态代码块都跑一遍。这也是为什么有时候你明明引入了某个类,但它对应的自动配置没生效——可能在第一步粗筛就被淘汰了。
4. 条件注解:自动配置最核心的裁决机制
4.1 条件注解是怎么挂到配置类上的
自动配置类再厉害,也不能无脑加载。它必须遵循一个原则:classpath里有相关依赖才配,用户自己配过就不再配。这个裁决机制就是条件注解。
条件注解的底层是Spring Framework 4.0就引入的@Conditional:
public interface Condition { boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }Spring Boot在此基础上封装了一套SpringBootCondition抽象类,并派生出各种@ConditionalOnXxx注解。比如@ConditionalOnClass背后是OnClassCondition,@ConditionalOnMissingBean背后是OnBeanCondition。
4.2 常用条件注解的判定逻辑一张表
| 注解 | 判定依据 | 典型使用场景 |
|---|---|---|
| @ConditionalOnClass / @ConditionalOnMissingClass | classpath中是否存在指定类 | 有MySQL驱动才配置数据源 |
| @ConditionalOnBean / @ConditionalOnMissingBean | 容器中是否存在指定Bean | 用户已手写DataSource,自动配置不再创建 |
| @ConditionalOnProperty | 配置项是否存在或等于指定值 | my.tp.enabled=true才启用线程池 |
| @ConditionalOnWebApplication | 当前应用是否为Web应用 | 仅在Web环境下注册MVC相关组件 |
| @ConditionalOnExpression | SpEL表达式结果 | 复杂多条件组合 |
| @ConditionalOnJava | JDK版本 | 按JDK版本提供不同实现 |
| @ConditionalOnResource | 指定资源是否存在 | 有某个类路径资源文件才加载 |
| @ConditionalOnSingleCandidate | 容器中某种类型只有一个可匹配候选Bean | 兜底配置,防止多实现冲突 |
举个例子,数据源自动配置的典型写法:
@AutoConfiguration @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) public class DataSourceAutoConfiguration { }只有classpath中同时存在javax.sql.DataSource和EmbeddedDatabaseType,这个配置类才进入正题。如果你的项目里根本没有spring-boot-starter-jdbc,它连被加载的资格都没有。
4.3 条件阶段:有些Bean要等到注册时才能裁决
关于条件注解,有一个容易被忽略的关键点:不是所有条件都在同一个阶段判断。
ConfigurationClassParser解析配置类时是分阶段进行的。ConfigurationCondition接口里有ConfigurationPhase这个概念:
public interface ConfigurationCondition extends Condition { ConfigurationPhase getConfigurationPhase(); }PARSE_CONFIGURATION:解析配置类阶段。REGISTER_BEAN:注册BeanDefinition阶段。
@ConditionalOnMissingBean这类条件必须放在REGISTER_BEAN阶段执行。原因很简单:在解析配置类的过程中,用户类的@Bean方法可能还没注册到BeanFactory里,这时候你去查“容器里有没有某个Bean”,大概率查不到,条件就会误判。
Spring Boot源码里,OnBeanCondition实现的getConfigurationPhase()返回的就是REGISTER_BEAN。这保证了它在做“用户有没有配”的判断时,能看到完整的用户Bean定义。
4.4 一个容易踩的类加载坑
用条件注解时,最常见的坑是:在配置类的方法里直接引用了可选依赖的类。
@AutoConfiguration @ConditionalOnClass(name = "com.example.optional.FeatureClient") public class MyAutoConfiguration { @Bean public Object someBean(FeatureClient client) { return new Object(); } }看起来@ConditionalOnClass已经保证了类存在,但Spring解析配置类的时候,会尝试加载配置类本身。如果方法参数里有FeatureClient,而FeatureClient类真的不在classpath中,条件注解返回了false也没用——类加载这一步就抛NoClassDefFoundError了。
正确做法是:
- 条件注解里用
name属性或直接引用类,但要保证配置类本身不依赖可选类。 - 方法参数用
ObjectProvider<FeatureClient>,拿不到就不注入,自己兜底。
@AutoConfiguration @ConditionalOnClass(name = "com.example.optional.FeatureClient") public class MyAutoConfiguration { @Bean public Object someBean(ObjectProvider<FeatureClient> clientProvider) { FeatureClient client = clientProvider.getIfAvailable(); return new Object(client); } }这是自动配置开发者必须养成的习惯:让条件自己判断,别让类加载机制替你做决定。
5. 追一条完整链路:什么都不配,DataSource是怎么来的
5.1 装配入口:DataSourceAutoConfiguration
讲了这么多理论,看一条实际链路最直观。假设你在一个新建的Spring Boot项目里引入了spring-boot-starter-jdbc,application.yml里只写了数据库连接参数,甚至干脆什么数据库参数都没写,应用也能启动——这个DataSource是怎么冒出来的?
入口就是DataSourceAutoConfiguration。放在常见Spring Boot版本里看,它的核心声明大致是这样:
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @EnableConfigurationProperties(DataSourceProperties.class) public class DataSourceAutoConfiguration { // 内部按条件导入具体的DataSource配置 }@ConditionalOnClass保证classpath里至少有JDBC的DataSource接口和嵌入式数据库类型类。引入spring-boot-starter-jdbc后,这两类肯定都有,所以配置类会进入后续流程。
5.2 属性进容器:@EnableConfigurationProperties与宽松绑定
那spring.datasource.url这些配置是怎么和DataSource关联起来的?答案在DataSourceProperties。
@EnableConfigurationProperties(DataSourceProperties.class)会注册一个配置属性绑定类,它绑定的前缀是spring.datasource:
@ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties { private String url; private String username; private String password; private String driverClassName; // getter/setter 省略 }这里有个Spring Boot独有的“宽松绑定”规则,值得说一下。url这个属性,你在配置文件里写成spring.datasource.url、spring.datasource.URL、spring.datasource.my-url这种风格,都能绑上。传统XML里,Bean属性名必须精确匹配;自动配置里则宽容得多。
底层是ConfigurationPropertiesBindingPostProcessor在容器启动时把Environment里的占位符解析后,按类型反射填充到DataSourceProperties对象里。这个过程完全不需要你手写任何@Value标签。
5.3 条件分流:内嵌库与连接池怎么选择
到了真正创建DataSource的环节,自动配置内部还会再分几条支线:
- 如果classpath里有内嵌数据库(H2、HSQL、Derby),且没有配置
spring.datasource.url,说明你大概率只是想跑个内存库,它会创建一个嵌入式数据源。 - 如果classpath里有连接池类(默认是HikariCP),且配置了数据库url,它会构建一个带连接池的DataSource。
- 如果classpath里同时有多个连接池实现,它会根据优先级选择,HikariCP优先。
这个“分流水”的逻辑,本质还是条件注解。Spring Boot内置的很多自动配置类,并非只长一张脸,它内部会用@ConditionalOnClass、@ConditionalOnProperty、@ConditionalOnMissingBean对场景做精细拆分。这也是为什么你给maven加一个依赖坐标,应用行为可能立刻发生变化——依赖本身就成了配置的一部分。
5.4 用户优先:@ConditionalOnMissingBean的退出机制
现在考虑另一种情况:你自己在业务代码里定义了一个DataSourceBean。
@Configuration public class MyDataSourceConfig { @Bean public DataSource dataSource() { // 自定义数据源逻辑 } }此时自动配置会怎么做?答案是:自动配置里的@ConditionalOnMissingBean(DataSource.class)判断出容器里已经有了这个Bean,于是放弃注册,让你手写的DataSource生效。
这个“用户优先”的兜底原则,是自动配置设计中最妙的地方之一。自动配置类在DeferredImportSelector里被延迟导入,所以它能看到所有用户BeanDefinition。它会先问自己:用户已经解决了这件事吗?解决了,我就退;没解决,我才上。
这也是为什么官方一直不建议你在自己的业务配置里覆盖自动配置的Bean——除非你真的知道自己在做什么。多数时候,你只需要修改application.yml里的配置项,让自动配置用你的参数创建Bean即可。
6. 动手实现一个自定义自动配置:把公司内部组件做成Starter
6.1 场景:想把内部线程池组件做成一键集成
理解了自动配置的运转,最有价值的落地方式就是自己写一个Starter。举一个实际场景:公司内部有个动态线程池组件ThreadPoolManager,原本每个项目接入都要手动创建Bean、读配置、注册监听。我们希望做成一个依赖坐标,引入后只写几行配置,组件就自动可用。
这个目标正好是自动配置最擅长解决的事。
6.2 代码结构:自动配置主类、配置属性、候选列表
项目结构大致是这样:
mycommon-spring-boot-starter/ ├── pom.xml └── src/main/resources/ └── META-INF/ └── spring/ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports自动配置主类:
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @AutoConfiguration @ConditionalOnClass(ThreadPoolManager.class) @ConditionalOnProperty(prefix = "my.tp", name = "enabled", havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties(TpProperties.class) public class TpAutoConfiguration { @Bean @ConditionalOnMissingBean public ThreadPoolManager threadPoolManager(TpProperties properties) { return new ThreadPoolManager(properties.getCore(), properties.getMax()); } }配置属性类:
import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "my.tp") public class TpProperties { private int core = 8; private int max = 32; // getter/setter 省略 }三个条件注解各司其职:
@ConditionalOnClass(ThreadPoolManager.class):线程池组件不在类路径时,整个配置类不生效。@ConditionalOnProperty(...):用户可以不写my.tp.enabled=true,默认仍然生效,方便组件即插即用。@ConditionalOnMissingBean:用户自己定义过ThreadPoolManager时,不重复创建。
6.3 注册候选清单与编译期元数据优化
然后在imports文件里登记:
com.example.mycommon.TpAutoConfiguration这一步必不可少。你光有个@AutoConfiguration类,但没写进imports清单,Spring Boot根本找不到它。
还有一个小技巧:在Starter的pom.xml里加上spring-boot-autoconfigure-processor:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> </dependency>这个注解处理器会在编译期扫描你的自动配置类,生成META-INF/spring-autoconfigure-metadata.properties。文件内容大致像这样:
com.example.mycommon.TpAutoConfiguration.ConditionalOnClass=com.example.mycommon.ThreadPoolManager有了这部分元数据,Spring Boot启动时能在最短时间内把不满足条件的自动配置类过滤掉,避免加载到一半才发现条件不满足。别小看这个优化,自动配置类数量多了以后,启动速度差距还是很明显的。
6.4 命名、放置与设计建议
关于自定义自动配置,我总结了几条实际经验:
- 类名建议以
AutoConfiguration结尾,并且放在独立的autoconfigure包下,不要放在业务Controller、Service旁边。否则容易被@ComponentScan扫到,造成重复注册。 - 自动配置类里的条件,尽量依赖接口或抽象类,而不是具体实现类。你永远不知道用户会用哪个实现替换你。
- 给用户留退出开关。默认生效是可以的,但一定要有
@ConditionalOnProperty或类似机制让用户可以关闭。没人喜欢一个“关不掉的开关”。 - 如果你在同一个Starter里提供了几个互相独立的自动配置类,建议用
@AutoConfigureBefore、@AutoConfigureAfter在类上声明顺序,不要依赖加载顺序的“巧合”。
7. 自动配置不生效时,照着这个思路查
7.1 开启条件评估报告
不管自己写Starter,还是排查第三方自动配置问题,第一步永远是看条件评估报告。
在application.properties里加一行:
debug=true或者启动时加个参数:
java -jar myapp.jar --debug启动日志里就会打印一份CONDITIONS EVALUATION REPORT,分为Positive matches和Negative matches。
- Positive matches:哪些自动配置类生效了,每个生效条件是什么。
- Negative matches:哪些自动配置类没生效,具体哪个条件没满足。
这是排查自动配置问题最重要的工具。我处理过的绝大部分“为什么没生效”问题,看这个报告五分钟就能定位。
7.2 一次典型的“自动配置没生效”排查
举个例子,有个项目反应MyBatis的Mapper扫描不正常,业务对象注入Mapper时直接报错。排查思路可以这么走:
- 先确认
mybatis-spring-boot-starter是否在依赖里,版本是否匹配Spring Boot版本。 - 看启动日志里
MybatisAutoConfiguration是否在Positive matches里。如果它在Negative matches里,仔细看它列出的未命中原因。 - 常见原因之一是
@ConditionalOnClass没找到某个类。这时检查依赖坐标是不是被exclusions排掉了,或者写的是provided作用域导致运行时没有。 - 另一个常见原因是你自己的
@Configuration提前定义了一个和自动配置冲突的Bean,导致@ConditionalOnMissingBean判断失败。
整个过程不需要去翻XML,也不需要乱加配置项。把报告当作“自动配置的体检单”,一项项对照即可。
7.3 三个真实的翻车现场
我一直觉得,坑踩得越具体,后面的印象越深。这里分享三个我在实际项目里遇到过的翻车案例。
第一个是Spring Boot 3.0升级问题。某个自研组件升级前一直正常,升级后静默失效。查了半小时才发现,它的jar包里还在用老式spring.factories注册自动配置。之前说过,3.0的自动配置不再读这个文件。解决办法就是改成imports文件,然后重新打包。
第二个是类加载异常问题。一个开发者在自动配置类里写了可选类的具体参数,见我之前写的那个ObjectProvider之前的例子。本地编译没问题,但测试环境里少了依赖后,启动直接抛NoClassDefFoundError。最后改成ObjectProvider兜底,条件注解才真的“只做条件判断”。
第三个是顺序问题。项目里有两个自动配置类,A依赖B创建的某个Bean,而B的加载顺序排在了后面,A的条件又引用了那种类型,导致A提前失败。后来在B的类上加了@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE),问题才缓解。这类问题在CONDITIONS EVALUATION REPORT里表现得很隐蔽,往往是“条件匹配了但还是没生效”,这时候就要回头检查@AutoConfigureBefore/@AutoConfigureAfter的顺序声明。
| 症状 | 可能原因 | 入手排查点 |
|---|---|---|
| 项目升级后组件失效 | spring.factories旧注册方式在3.0不被支持 | 确认imports文件存在 |
| 启动报NoClassDefFoundError | 配置类直接引用可选依赖类 | 改为ObjectProvider或name条件 |
| 条件显示匹配但Bean没创建 | 自动配置顺序导致提前失败 | 检查@AutoConfigureBefore/After |
| 自动配置没走,日志里没有报告 | debug未开启 | 开启debug查看Negative matches |
最后再说几句
自动配置这套机制,说到底是把“配置复杂度”从使用方转移到了框架设计方。你用到的每一个开箱即用的Starter,背后都是作者把成千上万种变量考虑掉之后,留下的那个“默认选项”。我个人认为,真正掌握自动配置的最佳路径,不是反复去看源码解读,而是亲手写一个哪怕很小众的自定义Starter。当你自己开始为“什么条件下才生效”“用户会不会覆盖我的Bean”“顺序会不会出问题”发愁时,那些原来记不住的类名和注解,就都钻进脑子里了。等你的项目再遇到自动配置相关的幺蛾子,你大概率会像我一样,第一反应不是上网搜,而是打开启动日志看那份条件评估报告——因为答案早就摆在那儿了。