第一次真正自己动手去翻 Spring Boot 自动配置源码的时候,我印象很深。当时项目出了个诡异的问题:本地 Redis 连得好好的,一上测试环境就抛 bean 不存在,报错信息里说的是 StringRedisTemplate 没注入进去。我第一反应是代码写错了,排查了半天,最后发现是测试环境里少了 spring-boot-starter-data-redis 这个依赖,而代码里压根没手写过任何 Redis 相关的 @Bean。
那个瞬间我突然意识到一件事:Spring Boot 的“自动配置”不是靠什么魔法,它就是一个很具体的代码逻辑,能不能生效取决于 classpath 里有哪些依赖、容器里有没有用户自定义的 Bean、配置文件里写了什么属性。澄清完整套逻辑之后,类似的故障基本就不用再靠猜了。自动装配(auto-configuration)是 Spring Boot 最核心的,也是最值得花时间彻底搞懂的机制。
这篇文章我会从自动配置到底在解决什么问题开始讲起,然后把启动入口、配置清单的读取过程、条件注解的匹配机制、排除自动配置的方式一个个拆开,中间会结合源码和实际文件说明。后面半部分我会用一个通过 Maven 手动搭建的 Spring Boot 工程,配合 Actuator 来观察到底哪些自动配置生效、哪些没生效,再用 Micrometer 监控、Redis Stream 拉消息、Firebase 推送集成这几个高频场景解释“自动配置能做什么、不能做什么”。无论你是刚上手的新手,还是打算把 Spring Boot 基础嚼碎了的面试候选人,这篇应该都能给你提供一点新的视角。
1. 自动装配不是魔法,是一套条件化的 Bean 注册方案
1.1 回看没有自动装配时的做法
我刚开始用 Spring 的时候,光搭一个带数据源的项目就要写一大堆配置。要么是到处堆 XML<bean>标签,要么是每加一个模块就得手写一个 @Configuration 类,把 RedisTemplate、RestTemplate、ObjectMapper 之类的 Bean 一个个注册进去。这套流程能做,但非常枯燥,而且每个项目的写法还不一样,换个团队就得重新对一遍约定。
自动配置的本质,其实就是把“根据依赖情况创建通用 Bean”这个动作标准化。底层仍然是 @Configuration 和 @Bean,但外面套了一层条件判断:判断 classpath 里有没有某个类,判断容器里是否已经有同类 Bean,判断 yml 里是否配置了某个开关。满足条件才注册,不满足就跳过。因此它不是一个独立的、神秘的新容器机制,它就是 Spring 容器之上的一套约定管理方式。
1.2 自动配置类与普通 @Configuration 的差异
从代码形式上看,自动配置类和普通配置类长得很像:
@AutoConfiguration @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { return new StringRedisTemplate(connectionFactory); } }注意看,RedisAutoConfiguration 和普通 @Configuration 的区别主要在两个点上。第一,它使用了 @AutoConfiguration 这个 Spring Boot 2.7 之后单独拆出来的注解,而不是直接使用 @Configuration。第二,自动配置类上的 @ConditionalOnClass、@ConditionalOnMissingBean 注解非常密集,几乎每个 @Bean 方法上都挂着条件。
这个设计背后有一个很实在的目的:自动配置类是框架层提供的“缺省值”,是兜底的方案。如果你自己在项目里已经定义了一个 RedisTemplate,那框架就不应该再注册同名 Bean,否则就会冲突或覆盖。@ConditionalOnMissingBean 保证了用户的优先级永远大于框架默认行为。想通这一点,后面看很多源码就不会被绕晕了。
2. 从启动类一路走到自动配置加载入口
2.1 一个注解的三重身份
随便打开一个 Spring Boot 项目,启动类基本长这样:
@SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }整个项目只加了一个注解,却能同时完成组件扫描、启动配置识别和自动配置的开启。因为 @SpringBootApplication 其实是一个组合注解,拆开来看是这样的:
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { ... }) public @interface SpringBootApplication { }- @SpringBootConfiguration:本质上就是 @Configuration,标识当前类可以作为配置类被加载。
- @ComponentScan:开启默认的包扫描。
- @EnableAutoConfiguration:自动配置的总开关。
其中最关键的是 @EnableAutoConfiguration。它告诉 Spring:在启动阶段,你不仅要把当前包下的 @Component 扫描进来,还要去 classpath 下加载所有约定好的自动配置类。
2.2 @EnableAutoConfiguration 是怎么把“开关”打开的
@EnableAutoConfiguration 的实现也很直接:
@AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { }核心是 @Import 了一个叫 AutoConfigurationImportSelector 的类。Spring 框架里只要你看到 @Import,就要意识到它是在向容器导入额外的配置类或注册组件。AutoConfigurationImportSelector 实现的是一个很关键的接口,叫 DeferredImportSelector。
为什么叫 Deferred,延迟执行?因为自动配置的加载时机不能太早。Spring Boot 需要先把项目里用户自己写的 @Configuration、@Component 全部扫描和处理完,至少让容器对现有 Bean 有一个基本判断,然后再去加载自动配置类。否则自动配置类上的 @ConditionalOnMissingBean 就没法判断用户到底有没有写过某个 Bean,条件判断就会失真。这个“先处理用户配置、再处理框架缺省配置”的顺序,是自动配置能优雅工作的一个隐藏前提。
3. 自动配置清单的存放与读取:spring.factories 和 AutoConfiguration.imports
3.1 配置文件的位置发生了变化
我们知道自动配置类是由 AutoConfigurationImportSelector 加载的,但它是从哪里知道该加载哪些类呢?答案不是硬编码,而是约定位置的文件。
早期版本(Spring Boot 2.7 之前),自动配置类的清单放在每个 jar 包里的 META-INF/spring.factories 文件里,内容大致长这样:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.starter.demo.DemoAutoConfiguration当 spring-boot-autoconfigure 这个包被扫到的时候,会从所有 jar 中读取 spring.factories,收集 EnableAutoConfiguration 键对应的所有类。如果你做过自定义 starter,一定对这段很熟。
从 Spring Boot 2.7 开始,官方引入了新的配置文件路径:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容是每一行写一个自动配置类的全限定名:
com.example.starter.demo.DemoAutoConfiguration com.example.starter.demo.OtherAutoConfigurationSpring Boot 3.x 已经彻底移除了从 spring.factories 中读取自动配置的逻辑,全面使用 AutoConfiguration.imports。之所以换文件,是为了减少干扰项、让自动配置的清单更清晰,同时避免 spring.factories 承载过多职责。
3.2 AutoConfigurationImportSelector 实际导入了什么
读取文件只是第一步。AutoConfigurationImportSelector 在处理的时候会做下面这些事:
- 拿到所有 classpath 下 AutoConfiguration.imports 里的类名列表。
- 对列表中的类做一次基础的排除,排除掉通过 exclude 属性指定的类。
- 结合 spring-autoconfigure-metadata.properties 之类的元数据进行快速过滤,先淘汰掉不可能满足条件的类。
- 最终把剩余的自动配置类封装成一组配置候选,再交给条件注解去做精筛。
Spring Boot 官方不建议在自动配置类被扫描的包里使用 @ComponentScan 去扫所有包,因为自动配置类几乎都会依赖外部类来做条件判断,一旦被普通组件扫描强行加载,很容易触发 NoClassDefFoundError。正确做法是自动配置单独放一个包,只在 AutoConfiguration.imports 里登记。
3.3 自动配置类的顺序怎么决定
自动配置类之间也存在依赖关系。比如很多配置类都要依赖 DataSource,那 DataSourceAutoConfiguration 就应该先执行。顺序控制也有专用注解:
- @AutoConfigureOrder:设置整体顺序,数字越小越先执行。
- @AutoConfigureBefore:声明当前类要在哪个类之前执行。
- @AutoConfigureAfter:声明当前类要在哪个类之后执行。
对比一下普通 @Configuration 的加载顺序不能随便猜,自动配置通过这类元注解把排序规则写得比较明确。AutoConfigurationImportSelector 在整理候选配置时,会把配置类分组排序,这一点在类名 AutoConfigurationSorter 里也能看出来。实际编码的时候,如果你自定义 starter 里的自动配置依赖了框架自带的配置,建议用 @AutoConfigureAfter 显式声明依赖,而不是靠运气。
4. 条件装配是自动配置能落地的关键
4.1 最常见的几类条件注解
自动配置类的代码里,随处可见各种 @Conditional 开头的注解。它们是判断是否注册 Bean 的“闸门”。我用一个表格把常用的列一下:
| 条件注解 | 判断依据 | 典型使用场景 |
|---|---|---|
| @ConditionalOnClass | classpath 里是否存在指定类 | 有没有引入某个 starter 或客户端 |
| @ConditionalOnMissingClass | classpath 里是否不存在指定类 | 依赖冲突时做降级 |
| @ConditionalOnBean | 容器中是否已有某种 Bean | 用户自定义 Bean 之后框架不再重复注册 |
| @ConditionalOnMissingBean | 容器中是否缺少某种 Bean | 框架默认 Bean 的兜底逻辑 |
| @ConditionalOnProperty | 配置文件里是否包含指定属性 | 通过开关控制功能是否启用 |
| @ConditionalOnWebApplication | 当前是不是 Web 应用 | 区分 Web 与非 Web 场景 |
| @ConditionalOnExpression | SpEL 表达式的计算结果 | 多个条件叠加判断 |
以 Redis 自动配置为例,如果项目里没有加入 spring-boot-starter-data-redis,那么 classpath 里就不会有 RedisOperations 类,@ConditionalOnClass(RedisOperations.class) 条件不成立,整个 RedisAutoConfiguration 就会被跳过,自然也不会有 RedisTemplate。这就是第一节那个故障的真正原因。
4.2 条件注解执行的时机和容易踩的坑
这里有一个特别值得说的细节。你在类上写 @ConditionalOnClass(SomeClass.class) 时,如果 SomeClass 这个类不存在,JVM 在加载注解的时候未必会立即报错,但一旦字节码层面需要解析这个类,就很可能抛 NoClassDefFoundError。为了避免风险,很多框架类在写条件注解时,会优先使用 name 属性:
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisOperations")这样条件判断完全基于字符串类名,不会主动触发类加载,安全性更高。Spring Boot 自动配置内部有一层提前过滤机制,使用 spring-autoconfigure-metadata.properties 保存各个自动配置类的关键条件,从而在真正加载配置类之前就先筛选掉一批不匹配的候选。不过这些属于框架内部优化,理解成“条件判断需要快速、安全和可靠”就够了。
还有一个容易踩的坑:@ConditionalOnMissingBean 判断的是当前容器已有的 Bean,但如果你把这个自动配置类放在一个用户配置类后面加载,容器里已经存在了某些早期注册的 Bean,判断结果就跟你以为的不一样。所以条件注解不能乱用在普通 @Configuration 的方法里,它更适合用在自动配置这个加载次序相对规范的场景。
4.3 手动排除自动配置的几种方式
有时候自动配置也不是越多越好。比如引入了某个 starter 之后它的自动配置不符合项目需求,我们需要手动把它排除掉。Spring Boot 提供了三种常见姿势。
第一种是在启动类注解上指定排除:
@SpringBootApplication(exclude = RedisAutoConfiguration.class) public class DemoApplication { }第二种是在 yml 文件里配置:
spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration第三种是条件排查,如果自动配置本身支持开关属性,也可以通过参数来控制,但这个要依赖于具体 starter 有没有提供开关。第一种和第二种是最通用的,注意排除时一定要写完整的类全限定名,排列时少一个包名就是 ClassNotFound。
5. 亲手用 Maven 搭一个工程,观察自动配置的整个过程
5.1 工程和依赖怎么搭
纯看原理容易看得云里雾里,我建议你亲手拉一个 Maven 工程,把自动配置的“现场”看一遍。这是最能加深理解的方法。我演示的是最基本的手工 Maven 项目,没有直接用 start.spring.io 生成也可以,但手工搭建一次你会对 pom 结构更敏感。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.3.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>auto-config-demo</artifactId> <version>1.0.0</version> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-registry-prometheus</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>加入 spring-boot-starter-parent 之后,大部分依赖版本都不用操心。spring-boot-starter-actuator 是监控端点,micrometer-registry-prometheus 是用来配合 Observable 指标收集的注册中心,这个组合在生产环境里也经常碰到。
5.2 通过 Actuator 查看自动配置匹配结果
写一个极简单的启动类和 Controller:
@RestController public class DemoController { @GetMapping("/hello") public String hello() { return "hello"; } }然后配置文件 application.yml:
management: endpoints: web: exposure: include: "*"启动项目,访问/actuator/conditions端点,你会看到很长一段 JSON,里面把当前启动过程中每一个自动配置类的判定结果都列了出来。
先看 Positive matches,这部分是条件成立、成功装配的自动配置,比如 ServletWebServerFactoryAutoConfiguration、DispatcherServletAutoConfiguration 这些。再看 Negative matches,这部分是未装配的,每一项后面还会标出触发未匹配的条件。比如没有引入数据源依赖时,DataSourceAutoConfiguration 就出现在这里,后面条件是 @ConditionalOnClass 未找到指定的 DataSource 相关类。
查询时我习惯直接过滤这几个关键字:
curl http://localhost:8080/actuator/conditions | jq '.contexts.defaults."持续写入中,下文待继续
后续更多内容请查收,下面是完整的技术拆解部分和实战经验总结。