别再回答“加 starter 就能用”了!把 Spring Boot 4 自动配置原理彻底撕透
公众号名称:Fox爱分享
作者名称:Fox爱分享
发布时间:2026-07-01 07:00
2016 年那会儿我做的还是老 Spring MVC 项目。你如果经历过那个时代会知道,单是一个数据源配下来,XML 能写上百行。DataSource、SessionFactory、TransactionManager,每一个 Bean 都得自己声明,少一个就启动报错。接手别人搭好的项目重建环境,那是真的噩梦。
后来 Spring Boot 来了。加一个 spring-boot-starter-data-jpa,啥也不用干,数据源自己就有了,连 H2 内嵌数据库都给你备好。你明明什么都没写,但它就是知道你要什么。
这个自动配置机制,从 Spring Boot 1.0 开始就是它最核心的卖点,用了十多年了。但说实话,大部分人对它的理解也就停在「加 starter 就能用」这个层面。再往下问几句,AutoConfigurationImportSelector 怎么找到那些配置类的、条件过滤怎么决定谁生效谁跳过、为什么你自己的配置永远能覆盖默认值——能说清楚的人不多。
Spring Boot 4 去年底发布之后,自动配置模块做了一次大拆解,从原来一个超大单体 jar 拆成了按技术栈划分的独立模块。这个变化让我重新翻了一遍源码和文档,把整条链路的原理从头到尾梳理了一遍。
不是什么魔法,是一套被精心雕琢了十多年的条件过滤引擎。
今天试着把它讲透。
你什么都没写,它怎么做到的
先从一个最简单的启动类说起。
每个 Spring Boot 项目都有一个带有 @SpringBootApplication 注解的启动类,你写一遍就再也不会改了。如果你把这注解拆开看,里面其实是三个东西:@SpringBootConfiguration,@ComponentScan,和 @EnableAutoConfiguration。
前两个好理解,就是个配置标记和组件扫描。
第三个,@EnableAutoConfiguration,是整个自动配置的总开关。
你自己写的 @Configuration 类,那是你手动告诉 Spring「我要这些 Bean」。自动配置不一样,你什么也没说,它自己推断出你要什么。
那它是怎么推断的呢?
@EnableAutoConfiguration 里面藏了一个 @Import:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}
它导入了一个类,叫 AutoConfigurationImportSelector。
这个类是整个机制的心脏,你可以把它理解成一个「总调度员」。它在 Spring 容器启动的时候负责收集所有候选的自动配置类,然后逐个判断要不要加载。
但这里有一个非常关键的细节。
AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口。
Deferred 什么意思?延迟。它不会在读到 @EnableAutoConfiguration 的第一时间就去执行,而是等你的所有自定义 @Configuration 都处理完了再跑。这个设计决定了自动配置的第一原则:
你的配置永远优先。
你自己声明了一个 DataSource Bean,它就不再帮你创建了。你自己配了一个 RestTemplate,它就不再给你默认的了。框架可以帮你补位,但它永远不会篡位。
我觉得这个设计特别好。
你去看太多框架,默认行为一旦和用户的配置产生冲突,第一反应是「我的规则你遵守就行」。Spring Boot 不是,它说「你说什么就是什么,只有你没说的时候我才帮你猜」。
说真的,这种对开发者自主权的尊重,放在任何一个技术产品里都是稀缺的。
继续说调度的过程。
AutoConfigurationImportSelector 启动后,会去扫 classpath 上所有 jar 包里面的一个特定文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
这个文件放在每个自动配置模块的 jar 包深处,格式极其简单,每一行就是一个全限定类名:
com.xxx.DataSourceAutoConfiguration
com.xxx.WebMvcAutoConfiguration
com.xxx.SecurityAutoConfiguration
就这三行,加起来的字符串可能不到两百个字符。但你要知道,你在 pom.xml 里加的每一个 starter,最终都会引入一个藏着这个 imports 文件的 jar 包。
然后 Spring Boot 把它们全部收集起来,合并成一个庞大的候选列表。
你加了一个 JPA starter,它就把 JPA 相关的候选加进去。你又加了 Web starter,又把 Web 相关的候选加进去。什么也没加,那就只有一个最基础的核心集合。
这一步只是收集。接下来才是好玩的地方。
不满足条件的,淘汰。
三层筛网,筛出你真正需要的
条件过滤这个东西,是整个自动配置设计里最精彩的部分。
你想想看,你加了一堆 starter,候选列表可能有几百个自动配置类。但显然不是每个都会生效。你没用 RabbitMQ,就不该初始化 RabbitMQ 的连接工厂。你没写 spring.mail.host,就不该创建邮件发送器。
那 Spring Boot 怎么知道谁该留谁该走?
靠的是 @Conditional 系列注解。
这些注解挂在每一个自动配置类上面,Spring Boot 启动时逐条评估,每条评估的结果只有两个,「通过」或「不通过」。你可以把它们理解成一层层筛网,留下来的都是你需要的东西。
这里面有一个最经典的案例,我觉得讲完它你就全明白了。
DataSourceAutoConfiguration。
它的判断逻辑分了三个层次。
第一个层次,@ConditionalOnClass。 它先检查 classpath 上面有没有 DataSource 这个类。有,说明你确实引入了数据库相关的依赖,值得它继续往下判断。没有,直接跳过,门都不给你开。
你是不是数据应用,classpath 从来不会骗人。
第二个层次,@ConditionalOnMissingBean。 检查容器里面是不是已经有你自己声明的 DataSource Bean 了。如果你自己写了一个,它就不动了。你写的就是最好的,我不碰。只有你什么都没声明的时候,它才继续往下走。
第三个层次,@ConditionalOnProperty。 检查你的配置文件里有没有写 spring.datasource.url。写了,它就连接你指定的外部数据库。没写呢,就给你搞一个内嵌的 H2 数据库,让你本地开发的时候不用操心数据库的事。
三层判断,一环扣一环。
如果你去看源码,这三个层次直接写在注解上,一目了然:
@AutoConfiguration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(DataSource.class)
static class EmbeddedDatabaseConfiguration {
@Bean
@ConditionalOnProperty(name = "spring.datasource.url", matchIfMissing = true)
DataSource dataSource() { ... }
}
}
类上的 @ConditionalOnClass 是第一层,内嵌配置类上的 @ConditionalOnMissingBean 是第二层,@Bean 方法上的 @ConditionalOnProperty 是第三层。
而且你仔细品品。第一层是「你需要吗」,第二层是「你已经有了吗」,第三层是「你要哪一种」。这个顺序不是随便排的,是按照「从宏观到微观,从通用到具体」的逻辑严格设计的。
你把几百个候选类的条件评估全部跑一遍,最后留下生效的那些,恰好就是你的应用真正需要的。不多不少。
谁先谁后,不是小事
到这里还没完。
几百个候选类里,有一批通过了条件过滤,但它们之间是有依赖关系的。
JdbcTemplate 自动配置必须在 DataSource 自动配置之后运行。没数据源你初始化什么 JdbcTemplate?WebMvc 自动配置必须在 DispatcherServlet 自动配置之后跑,否则连 DispatcherServlet 都没注册,Web 层配置什么。
所以 Spring Boot 还需要一套顺序控制机制。
从 Spring Boot 2.7 开始,@AutoConfiguration 注解就集成了排序能力,直接在注解属性上声明先后关系:
@AutoConfiguration(after = DataSourceAutoConfiguration.class)
public class JdbcTemplateAutoConfiguration { ... }
就这一行,after 属性直接声明在自动配置主注解上。除了 after,还有 before、afterName、beforeName 四个属性,覆盖了「类引用」和「全限定名字符串」两种声明方式,后者可以避免某些场景下的循环引用问题。
在这之前,排序靠的是单独的 @AutoConfigureAfter 和 @AutoConfigureBefore 注解,从 2.7 开始被统一收进了 @AutoConfiguration,代码少了一行,阅读时的信息密度却变高了。
但我必须强调一个容易被误解的细节。
这些排序注解控制的是 Bean「定义」的注册顺序,不是 Bean「实例」的创建顺序。实例化的顺序由 Bean 之间的依赖关系和 @DependsOn 决定,那是另一套机制。
很多人初学的时候把这两件事搞混,我刚开始也搞混过,搞清楚了之后才发现这个设计是合理的。定义顺序保证了配置的优先级正确,实例化顺序保证了运行时不出空指针。两套机制各司其职,互不干扰。
编译期做的事,别留给运行时
除了排序,还有一个容易被忽略的性能优化。
Spring Boot 在编译期通过一个注解处理器叫 spring-boot-autoconfigure-processor,预先分析了所有自动配置类上的 @Conditional 注解,生成了一个元数据文件:
META-INF/spring-autoconfigure-metadata.properties
这个文件里面存的是所有条件判断的摘要信息:
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.ConditionalOnClass=javax.sql.DataSource
比如这一行,它告诉你 DataSourceAutoConfiguration 依赖 DataSource 类在 classpath 上。不用解析注解,不用反射,读一行字符串就知道这个类能不能用。
启动的时候,Spring Boot 拿这个元数据先做一轮「预过滤」。这轮过滤不需要加载类,不需要反射,不需要实例化,纯读一个 properties 文件就能快速排除掉一大半明显不满足条件的候选类。剩下的那些,再去走真正的条件评估流程。
这一步对启动时间的优化效果非常显著。
你想想这个设计的妙处。它不是在启动时「做更多事」,而是在启动前已经把部分工作「提前做掉了」。编译期生成的元数据文件,像是一份地图,告诉启动器「别费劲往那几个方向走了,都是死路」。
把时间花在编译期,比花在运行时划算一万倍。
以上是原理部分,版本无关,从 1.0 到 4.0 一脉相承。接下来聊聊 Spring Boot 4 真正改了什么。
Spring Boot 4:拆掉那座单体大山
上一个版本的 spring-boot-autoconfigure 是一个超级大单体 jar 包。Web、JPA、Kafka、Redis、AMQP、Security 等等等等,所有自动配置类全部堆在一个 jar 里。
这带来一个什么问题呢?
你做了一个只连数据库的纯后端服务,Kafka 的自动配置类、Web 的自动配置类、消息队列的自动配置类,全都在你的候选列表里。虽然条件过滤最终会把它们排掉,但扫描这几百个候选类、读取元数据做预过滤的过程,该花的 CPU 时间一分没少。
Spring Boot 4 把这件事彻底改了。
他们把 spring-boot-autoconfigure 按技术栈拆掉了——官方博客原文的说法是「将单体 spring-boot-autoconfigure jar 拆分为更小、更聚焦的模块」,实际拆分成了数十个独立模块。WebMVC 自动配置是一个独立模块,WebFlux 是另一个,JPA 是另一个,AMQP、Kafka 各是各的。你引入 spring-boot-starter-data-jpa,就只拿到 JPA 相关的自动配置类。Web、Kafka、消息队列那些,根本不在你的 classpath 上,连候选都不是。
这就不是单纯的「快一点」了。
候选列表从几百个缩到几十个,条件评估的轮次大幅缩减,启动时的 CPU 占用肉眼可见地下降了。GraalVM 原生镜像的体积也显著缩小。
而且这还带来一个更细微但同样重要的好处。
你在 IDE 里配置自动补全的时候,不再被一堆你用不到的配置属性轰炸了。你只用 JPA,补齐列表里就不会冒出 spring.kafka.xxx 或者 spring.rabbitmq.xxx 这种跟你毫无关系的东西。开发体验的改善有时候就藏在这些不起眼的地方。
替换映射:升级时的安全网
模块拆分意味着很多自动配置类要改包路径、改类名。如果你之前在代码里用 exclude 引用过某个自动配置类,升级的时候会不会直接类找不到报错?
Spring Boot 4 准备了一个替换映射机制。在 META-INF/spring 目录下可以放一个 AutoConfiguration.replacements 文件,把旧类名映射到新类名。升级的时候框架自动帮你做路由,不会因为类路径变了就直接报错。
这和 Spring Boot 一直以来的升级策略一脉相承——尽量让升级无痛,能自动处理的绝不甩给开发者。
虚拟线程:自动配置哲学的延伸
说到 Boot 近几年的变化,有一个配置项值得单独提一下——虽然它实际上从 3.2 就引入了,不是 Boot 4 的功劳。
spring.threads.virtual.enabled,设成 true 之后,所有自动配置创建的线程池 Bean,@Async 用的任务执行器,@Scheduled 用的调度线程池,全部自动切换到虚拟线程。
你一行代码都不用改。
这件事跟自动配置的哲学是一脉相承的。好的框架就应该在你不需要了解细节的时候帮你把事情干了,但你需要掌控细节的时候又完全放权给你。虚拟线程的开关设计就是这样——你不想管的时候开一个配置项完事,你想自己定制的时候自己声明线程池,框架的自动配置自动退让。
自动配置教会我的事
研究完这整套机制,我有一个感受越来越强。
好的框架设计,本质上是一种带着适度怀疑的判断系统。
每一个自动配置类在生效之前,都要经过 @ConditionalOnClass 的检验——「你真的需要我吗?」;经过 @ConditionalOnMissingBean 的检验——「你是不是已经有更好的了?」;经过 @ConditionalOnProperty 的检验——「具体要哪一种?」。它不假设你「一定需要某个东西」,它只在你没说过「不」的时候,才试着帮你补上。
这种设计哲学远远超出了 Spring Boot 本身。
我们在做任何技术决策的时候,其实也可以套用这套三层判断。第一层,我们的 classpath 上真的有这个需求吗?第二层,我们是不是已经有一个能用的方案了?第三层,具体到配置细节,我们到底需要哪一个版本?
不是急着加新东西,而是先看看你现在有什么。不是急着引入新框架,而是先看看你手里这套能不能用。不是急着追最新版本,而是先看看你这个场景到底需不需要。
我有时候觉得,把自动配置研究透了的程序员,写代码的风格都会变。你会本能地对「自动」产生信赖,但同时也对「默认」保持警惕。你知道框架在背后帮你做了多少判断,也知道哪一步的判断你可以覆盖掉。
这种平衡感让我想起以前看《禅与摩托车维修艺术》里的一句话——「好的工具是一种你感觉不到它的工具」。
Spring Boot 的自动配置就是这样的工具。
你开发的时候感觉不到它在工作。但它一直在。
如果你喜欢这种把技术原理掰开揉碎讲透的方式,欢迎关注我的公众号「Fox爱分享」,每周会持续输出这类深度技术拆解。觉得有用的话,点个赞、在看、转发三连,想第一时间收到推送就星标一下。
内容效果不满意?点此反馈