我最早琢磨Spring Boot的自动配置时,心里就憋着一个问题:明明只是加了spring-boot-starter-web这一个依赖,内嵌Tomcat、DispatcherServlet、Jackson消息转换器就全都备好了,连数据源都帮你初始化了——这些bean到底是谁创建出来的?又是谁判断“当前项目需要这些配置”?
如果你也一直卡在“SpringBoot自动配置到底是怎么跑通的”这个问题上,那就对了。这篇就是为了把整条链路彻底拆开讲透而写的。我会从@SpringBootApplication后面的那几个注解开始,一直讲到条件注解的匹配机制、自定义starter的完整实现,以及自动配置失效时怎么一步一步排查。适合刚入门的同学建立整体认知,也适合用了Spring Boot一段时间、但始终对原理不太踏实的人查漏补缺。
1. 自动配置的核心链路:从启动到bean实例化中间发生了什么
1.1 @SpringBootApplication的“三层嵌套”到底嵌套了什么
很多人写第一个Spring Boot项目时都是复制粘贴一个@SpringBootApplication,然后就开始写Controller了。但如果你把它拆开看,会发现它其实是三个注解的组合:
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), ... }) public @interface SpringBootApplication { }逐个说:
@SpringBootConfiguration:本质就是@Configuration,只是Spring Boot给了它一个新名字。它标记这个类是一个配置类。@ComponentScan:启动类所在包及其子包会被扫描注册成bean。@EnableAutoConfiguration:这才是自动配置的开关,它负责触发整个自动配置加载流程。
前两个注解很容易理解,真正决定自动配置命运的是第三个。
1.2 自动配置加载的入口:AutoConfigurationImportSelector
@EnableAutoConfiguration的核心作用其实只有一个——通过@Import引入一个选择器:
@Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { }AutoConfigurationImportSelector是什么?它可以理解为“自动配置清单的搬运工”。Spring容器启动时会调用它的selectImports方法,这个方法会去读一批特殊的配置文件,把所有写在上面的自动配置类全捞出来,再返回给容器做后续注册。
整个流程大致是:
- 容器启动,解析到
@EnableAutoConfiguration。 AutoConfigurationImportSelector被实例化并执行。- 它利用
SpringFactoriesLoader机制加载classpath下所有META-INF/spring.factories(老版本)里的EnableAutoConfiguration配置项,或者Spring Boot 2.7之后新增的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。 - 对拿到的自动配置类名单做去重、排序,排除掉exclude指定的类。
- 把最终名单交给容器进行注册。
这里有一个容易忽略的细节:AutoConfigurationImportSelector实现的是DeferredImportSelector,不是普通的ImportSelector。这个“Deferred”意味着自动配置类的解析时机被推迟了,要等普通配置类、@ComponentScan扫描的bean都处理完之后,才轮到自动配置类。这样设计的原因很简单——自动配置往往是兜底方案,它需要先知道用户自己定义了哪些bean,才能决定自己要不要干活。
1.3 那些被加载的自动配置类到底在干什么
我拿最常见的DataSourceAutoConfiguration举例。这个类从头到尾只做一件事:判断当前应用是不是真的需要数据源。
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class) @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") @EnableConfigurationProperties(DataSourceProperties.class) @Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) public class DataSourceAutoConfiguration { }可以看到它身上挂满了条件注解。这些条件的含义是:
- classpath里有
javax.sql.DataSource和org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType,才考虑创建数据源bean。 - 当前容器里没有
ConnectionFactory类型的bean。 - 通过
@EnableConfigurationProperties(DataSourceProperties.class)加载spring.datasource前缀的配置。
也就是说,自动配置类本身不是无脑注册一堆bean,它更像一个“有判断力的管家”——先检查条件满不满足,再决定要不要动手。这一点理解透了,后面排查问题会轻松很多。
2. 条件注解:自动配置的执行开关与匹配时机
自动配置要想做到“智能”,靠的就是一组条件注解。我建议大家把这组注解当成一个家族来记,因为它们的名字、语义和适用场景是成体系的。
2.1 类路径条件:@ConditionalOnClass与@ConditionalOnMissingClass
这是最常见的条件。Spring Boot看到某个自动配置类之前,首先要确认相关依赖是不是真的在classpath里。
@ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver")这里的name可以填字符串,好处是不会因为类不存在而直接触发ClassNotFoundException。如果你的代码引用了不存在的类,JVM在加载配置类时可能直接报错,用字符串方式则安全得多。
实际项目里最常见的一个“坑”是:你的项目里明明引入了某个依赖,但条件还是没匹配上。这时候先别急着怀疑条件注解,优先检查是不是依赖传递被排除了。比如spring-boot-starter-web会自动引入Jackson相关依赖,如果你后来手动排除了jackson-databind,那么所有基于ObjectMapper的自动配置都会静默失效——这就是类路径条件带来的连锁反应。
2.2 Bean条件:@ConditionalOnMissingBean与@ConditionalOnBean
@ConditionalOnMissingBean是自动配置“让位”给用户的关键机制,它的含义是:如果用户已经手动声明了同类型的bean,我这边就不再注册了。
@Bean @ConditionalOnMissingBean(JdbcTemplate.class) public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); }这个逻辑在生活中很好理解:管家看到主人已经自己准备好了杯子,就不会再多拿一个杯子出来。
但@ConditionalOnBean要谨慎使用,因为它和BeanDefinition的注册顺序有很强的耦合。如果你在一个普通的配置类里用@ConditionalOnBean判断另一个配置类是否注册了某个bean,结果可能和你预期不一致——因为条件评估发生在bean实例化之前,此时某些bean可能还没注册完。Spring官方对@ConditionalOnBean的建议是:最好只在自动配置类里使用,并且配合@AutoConfigureAfter控制顺序。
2.3 属性条件:@ConditionalOnProperty
用来根据配置文件里的属性值决定是否启用某个配置。
@ConditionalOnProperty(prefix = "my.task", name = "enabled", havingValue = "true", matchIfMissing = true)matchIfMissing = true表示配置项缺失时也算匹配通过。这是一个很实用的设计,让功能默认开启,用户想关掉时只需显式设置enabled=false。反过来的场景也有:如果你希望某个功能默认关闭、用户显式开启,就把matchIfMissing设为false。
2.4 环境条件:@ConditionalOnWebApplication
用于区分当前应用是Web应用还是非Web应用。这个条件底层会去检查容器的类型,比如是不是ServletWebServerApplicationContext。做自动化配置模块时,经常需要用它来决定注册Web专属的组件(如过滤器、拦截器)还是不注册。
2.5 条件评估的顺序问题
自动配置类的加载顺序不是随机的。Spring Boot通过@AutoConfigureBefore、@AutoConfigureAfter和@AutoConfigureOrder这三个注解来维护顺序。简单理解,它们声明的是“我必须在谁之前干活”和“我必须在谁之后干活”。
顺序在自动配置里很重要,因为有的自动配置类依赖其他自动配置注册的bean。比如JdbcTemplateAutoConfiguration必须在DataSourceAutoConfiguration之后执行,否则JdbcTemplate创建时没有数据源可用。排序规则在加载阶段就会被读取并排序,最终形成一个有序的自动配置执行链。
3. 手写一个自动配置模块:完整可运行的starter示例
讲了半天原理,现在动手写一个真正能用的自动配置模块。我们从零做一个“短信发送器”的starter,让任何项目引入这个依赖后,只要配置了my.sms前缀的属性,就能直接用SmsSender。
3.1 工程结构设计
一般自定义starter会拆成两个Maven模块:
sms-spring-boot-starter:空壳模块,只依赖自动配置模块。用户引入这个即可。sms-spring-boot-autoconfigure:真正的自动配置逻辑所在。
为什么要拆开?因为如果用户想自己扩展底层实现,只需要依赖autoconfigure模块就够了,不必把starter里可能带的一些可选依赖一起带进来。这是Spring Boot官方推荐的命名和分层方式:xxx-spring-boot-starter+xxx-spring-boot-autoconfigure。
3.2 定义业务类和属性类
先定义一个最简单的短信发送服务:
public class SmsSender { private final SmsProperties properties; public SmsSender(SmsProperties properties) { this.properties = properties; } public void send(String phone, String content) { System.out.println("向 " + phone + " 发送短信:" + content); System.out.println("当前签名:" + properties.getSignName()); } }再写配置属性类,它负责把my.sms前缀下的配置值绑定成一个对象:
@ConfigurationProperties(prefix = "my.sms") public class SmsProperties { private String signName = "默认签名"; private String endpoint; private String accessKeyId; private String accessKeySecret; // getter / setter 省略 }@ConfigurationProperties是自动配置里最值得掌握的一个技巧,它把散落在配置文件里的my.sms.sign-name、my.sms.endpoint这些键,自动映射到Java对象字段上,免去你手写一堆@Value的麻烦。
3.3 编写自动配置类
自动配置类负责把前面两个类组装到一起,并加上条件控制:
@AutoConfiguration @ConditionalOnClass(SmsSender.class) @ConditionalOnProperty(prefix = "my.sms", name = "enabled", havingValue = "true", matchIfMissing = true) @EnableConfigurationProperties(SmsProperties.class) public class SmsAutoConfiguration { @Bean @ConditionalOnMissingBean public SmsSender smsSender(SmsProperties properties) { return new SmsSender(properties); } }这段代码的逻辑是:
@AutoConfiguration标记这是一个自动配置类。这是Spring Boot 2.7之后取代@Configuration的新注解,主要区别在于它专门用于自动配置,可以配合@AutoConfigureBefore / @AutoConfigureAfter使用。@ConditionalOnClass(SmsSender.class)保证类路径里有短信发送器才生效。@ConditionalOnProperty让用户可以通过配置关闭该功能。@ConditionalOnMissingBean给用户留了自定义覆盖的口子。
3.4 注册自动配置类
在resources目录下创建META-INF/spring.factories(老方式):
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.sms.autoconfigure.SmsAutoConfiguration或者用Spring Boot 2.7之后推荐的新方式,在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里写一行类名:
com.example.sms.autoconfigure.SmsAutoConfiguration两个文件可以同时存在,Boot会兼容读取。我建议新项目直接使用AutoConfiguration.imports方式,因为Spring Boot 3.x已经不再支持spring.factories方式注册自动配置了。
3.5 测试验证
在另一个Spring Boot项目里引入这个starter,然后配置:
my.sms.sign-name=我的小店 my.sms.enabled=true启动后注入SmsSender,调用send("13800000000", "您好"),控制台会输出短信内容和签名。如果你把enabled改成false再启动,程序就不会注册SmsSender——这时如果强行注入会在启动阶段报NoSuchBeanDefinitionException,这个表现也能反向验证自动配置确实受属性条件控制。
我第一版starter测试时还犯过一个低级错误:自动配置类的包路径和启动类包路径没有隔离,写在了com.example下,结果被@ComponentScan提前扫描到,手动注册了一遍。虽然功能能跑,但自动配置的"优先级让位"逻辑就没法生效了。所以自定义自动配置类的包路径一定要独立,比如放在com.example.sms.autoconfigure,不要放在启动类所在包及子包内。
4. 排查自动配置不生效:先用这四招定位问题
自动配置最折磨人的地方就是:项目能启动,但某个功能没生效,而且没有任何报错。这时候别瞎猜,直接用下面几个手段一层层排查。
4.1 自动配置报告:先看正反匹配结果
在application.properties里加一行:
debug=true启动后控制台会打印一份自动配置报告,其中最关键的是这两类:
Positive matches:表示条件匹配成功、已经生效的自动配置项。Negative matches:表示条件未匹配、没有生效的自动配置项。
如果你发现某个自动配置没有生效,第一时间应该去Negative matches里找到它,看看哪个条件把它的路堵死了。报告里会显示这个类上的所有条件评审结果和具体原因,比如“类不存在”“bean已存在”“属性不匹配”等等。
4.2 条件评审日志:@ConditionalOnXxx的具体判定细则
自动配置报告里的Negative matches通常只给出一句简短说明,如果还不够,可以打开条件评估日志的完整输出:
logging.level.org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener=debug这条配置会输出完整的ConditionEvaluationReport,包含每个自动配置类与所有条件注解的详细匹配记录。我在排查一个第三方SDK的自动配置时,就靠它发现是@ConditionalOnMissingBean(JdbcTemplate.class)被一个自研的包装类挡住了——因为包装类继承自JdbcTemplate,泛型推断后被认为同类型的bean已存在,自动配置便静默退出。
4.3 查看上下文里到底有哪些bean
如果配置报告显示Positive matches通过了,但功能还是不对,可能需要确认容器里实际注册的bean情况。
最直接的办法是注入ApplicationContext,写一个临时接口打印所有bean名称:
@RestController public class BeanListController { @GetMapping("/beans") public String listBeans() { String[] names = applicationContext.getBeanDefinitionNames(); return String.join("\n", names); } }当然,更正规的做法是引入spring-boot-starter-actuator,访问/actuator/beans查看bean信息。它能显示bean的类型、依赖、作用域等,比手动打印更详细。
4.4 配置绑定是否正确:检查ConfigurationProperties
有时候自动配置加载了,但属性值全是默认值,看起来像没生效。这多半是配置前缀写错了或者绑定失败了。
排查手段:引入actuator后访问/actuator/configprops,它会列出所有@ConfigurationProperties类的当前绑定值。比如你配了my.sms.sign-name,在这里就能看到它有没有成功绑定到SmsProperties.signName上。
特别注意Spring Boot对绑定器有“宽松绑定”规则:my.sms.sign-name、my.sms.signName、my.sms.sign_name都是同一个字段的合法写法。但如果是用@Value("${my.sms.signName}")这种注解去读属性,它就严格遵守键名匹配,不会帮你做宽松转换。
5. 自动配置的进阶细节与常见坑
5.1 自动配置和@ComponentScan的包扫描冲突
@ComponentScan会扫描启动类所在包及子包下所有@Component、@Configuration等标注的类。这意味着:
- 自动配置类如果恰好放在启动类所在包及子包内,会被当成普通配置类提前加载,从而打乱
DeferredImportSelector的延迟机制。 - 用户自定义的
@ConfigurationProperties类如果不在扫描路径里,且又没有通过@EnableConfigurationProperties显式注册,就不会成为bean。
所以自定义starter的源码分包里,自动配置类必须放在独立包,例如com.example.sms.autoconfigure,不要和示例代码混在同一个包结构里。
5.2 覆盖自动配置bean的正确姿势
官方推荐的方式是:在任意一个普通配置类里自己定义bean,并配合@ConditionalOnMissingBean的自动配置类实现覆盖。
@Bean public SmsSender mySmsSender() { return new MyCustomSmsSender(); }自己在配置类里定义了SmsSender之后,自动配置里的@ConditionalOnMissingBean条件就不匹配了,自动配置会默默退出。这套机制的好处是:你不需要修改第三方包的任何代码,也不需要排除整个自动配置类,就能完成替换。
另一种做法是使用exclude:
@SpringBootApplication(exclude = SmsAutoConfiguration.class)但我不建议一上来就用exclude,因为它是“一刀切”,会把这个自动配置类里的所有bean全部禁用。万一你只需要调整其中一个bean,手动配置是更温和的方案。
5.3 直接排掉一个bean和排除整个自动配置类的区别
有时候你会看到网上有人用@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })来阻止数据源自动配置。这可行,但要注意它只是阻止了DataSourceAutoConfiguration这个类执行,如果还有其他依赖它而创建的bean,比如JdbcTemplateAutoConfiguration,也会因为找不到数据源而连锁失效。排查的时候顺着自动配置报告的因果链一步步看,比逐个exclude更靠谱。
5.4 Spring Boot 2.x与3.x的自动配置差异
如果你从Spring Boot 2.x迁移到3.x,自动配置有两处变化需要特别注意:
- 注册文件从
META-INF/spring.factories迁移到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,3.x不再支持用spring.factories注册自动配置类。 - 自动配置类上的
@Configuration建议换成@AutoConfiguration,语义更精确,也能避免被普通组件扫描误伤。
同样的,条件注解的包路径也从org.springframework.boot.autoconfigure.condition调整了部分类的结构。升级时把这些地方逐一对照文档检查,能省下很多排查时间。
5.5 那些看起来在自动配置、实际是普通bean的例子
最后提一个容易混淆的地方。有些第三方库喜欢在自己包的@Configuration类里通过@Import引入自己的配置,并通过@EnableXxx注解对外开放。这类机制和Spring Boot的自动配置没有直接关系,并不会出现在自动配置报告里。排查这类库的生效问题时,用“查bean”的方式去定位,远比在自动配置报告里找更高效。
拿我自己的经验来说,检查一个自动配置是否生效最快的顺序就是:先看Positive/Negative matches确认它有没有被加载,再看configprops确认属性绑定有没有成功,最后看实际bean是不是注册了。三步走完,问题通常已经浮出水面。
6. 收尾:把自动配置真正当成一套机制来理解
写到这里,Spring Boot自动配置的整条链路应该已经比较清楚了:@EnableAutoConfiguration通过AutoConfigurationImportSelector去加载自动配置类清单,自动配置类再借助各种条件注解判断自己该不该生效,并且通过@ConditionalOnMissingBean把“优先权”让给使用者,最终配合@ConfigurationProperties完成配置的绑定。
这套机制让框架从“配置驱动”变成了“约定驱动”,也让第三方starter有了标准化的接入姿势。对我个人来说,最大的体会是:自动配置不是一个黑盒,而是一个有明确运行规则的系统。一旦你熟悉了条件注解的语义和加载顺序,很多看起来很玄的“为什么没生效”问题,其实都能通过报告和日志快速定位出来。
如果后面要深入研究,建议你自己动手仿照文中这个短信starter再做一个小而全的自动配置模块——比如一个简单的操作日志starter,或者一个校验器starter。自己实现一遍之后,再回去看Spring Boot源码里那些自带自动配置类,思路会一下子顺畅很多。