主题
Spring Boot 自动装配
一句话:Spring Boot 把“候选配置类清单”放进 classpath 的固定文件,启动时按条件筛选后注册为 Bean;换来引入依赖即可用,代价是装配结果隐式、排查要靠条件报告。
适用版本:Spring Boot 3.x(自动配置候选文件为 2.7+ 的新位置)。Spring Boot 2.7 之前用 spring.factories,3.0 不再用该文件承载自动配置。
为什么需要
不用自动装配时,引入一个 Web 框架要做这些事:
- 手工声明
DispatcherServlet、HandlerMapping、HandlerAdapter、HttpMessageConverter。 - 手工配数据源、连接池、事务管理器、MyBatis 的
SqlSessionFactory,每个项目复制一遍。
Boot 的做法是:每个 starter 把自己的自动配置类写进清单,启动时统一读取,再用 @ConditionalOnXxx 判断“当前 classpath、已有 Bean、配置属性”是否满足。满足就注册,不满足就跳过;开发者自定义的 Bean 优先。
核心机制
入口与候选清单
@SpringBootApplication
= @SpringBootConfiguration
+ @EnableAutoConfiguration
+ @ComponentScan@EnableAutoConfiguration 通过 @Import(AutoConfigurationImportSelector.class) 导入选择器。选择器读取:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件内容是一行一个自动配置类的全限定名,例如 org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration。
版本差异:
- Boot 2.7+ / 3.x:自动配置候选走上面的
AutoConfiguration.imports。 - Boot 2.7 之前:候选写在
META-INF/spring.factories的org.springframework.boot.autoconfigure.EnableAutoConfiguration键下。 - Boot 3.0 起:
spring.factories仍用于其他 SPI(如EnvironmentPostProcessor、ApplicationContextInitializer),但不再用于自动配置。
条件注解与顺序
| 注解 | 判断 | 典型用法 |
|---|---|---|
@ConditionalOnClass | classpath 是否有指定类 | 有 DataSource 才配数据源 |
@ConditionalOnMissingBean | 容器是否还没有该类型 Bean | 用户没自定义时才注册默认实现 |
@ConditionalOnBean | 容器是否已有指定 Bean | 有 DataSource 才配 JdbcTemplate |
@ConditionalOnProperty | 配置属性是否匹配 | spring.session.store-type 等开关 |
@ConditionalOnWebApplication | 是否 Web 应用 | 只给 Servlet/Reactive 应用装配 |
@ConditionalOnSingleCandidate | 该类型只有一个候选,或其中一个标了 @Primary | 多数据源时避免错误装配 |
两个容易忽略的点:
- 用户配置优先:自动配置类通过
DeferredImportSelector延迟处理,等用户自己的@Configuration解析完再评估条件,所以@ConditionalOnMissingBean能看到用户已注册的 Bean。 - 条件有顺序:
@ConditionalOnBean只能看到“已经处理过的” Bean 定义;把自动配置类的前后关系写清楚,用@AutoConfigureBefore/@AutoConfigureAfter,不要依赖偶然的类名顺序。
启动时序中自动配置与 WebServer 的位置
关键时序:
- 自动配置在 refresh 第 5 步解析,早于业务单例实例化(第 11 步)。所以自动配置里的
@Bean方法此时只是注册定义,真正创建对象要等到第 11 步。 - WebServer 在第 9 步
onRefresh()创建(Servlet 容器是ServletWebServerApplicationContext.createWebServer()),早于业务单例;但对外接受请求的启动动作在第 12 步,由WebServerStartStopLifecycle调用WebServer.start(),Tomcat started on port(s)日志出现在这里。 - 应用类型由
WebApplicationType.deduceFromClasspath()判断:classpath 有DispatcherServlet为 SERVLET,有DispatcherHandler为 REACTIVE,两者都没有为 NONE;这决定了创建哪种 ApplicationContext。 ApplicationRunner/CommandLineRunner在refresh()完成后执行,适合做预热、校验和一次性任务;不要在这里做长阻塞,否则启动探针会失败。
实战场景
一、NoSuchBeanDefinitionException:本地能起,生产起不来
现象:风控服务接入短信组件后本地正常,流水线构建出的镜像在生产启动即失败:No qualifying bean of type 'com.example.sms.SmsClient'。两个环境用同一份 application.yml,唯一差别是生产镜像由 maven-shade-plugin 打成 fat jar。
排查:Bean 不存在只有三种成因,条件报告能一次分开。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 自动配置是否被评估 | java -jar app.jar --debug,搜类名是否出现在 Positive/Negative matches | 两个列表都没有 → 候选清单根本没被读到 |
| 条件为何不满足 | Negative matches 里的说明,如 @ConditionalOnClass did not find required class | 缺类 → 依赖或打包问题 |
| 是否被用户 Bean 顶掉 | 提示 @ConditionalOnMissingBean found beans | 自己声明过同类型 Bean,默认实现退出 |
| 清单文件是否在包里 | unzip -l app.jar | grep AutoConfiguration.imports,再 unzip -p app.jar <path> 看内容 | 文件缺失或多行被合并 → 打包阶段丢内容 |
根因是第四行:shade 打包时对 META-INF/spring/ 下的同名文件用了默认合并策略之外的覆盖规则,多个 starter 的 AutoConfiguration.imports 互相覆盖,只剩最后一个。Boot 2.7 之前的 spring.factories 有 AppendingTransformer 处理这类合并,新路径的清单文件常被打包配置漏掉。
对症做法:
xml
<!-- shade:按追加方式合并,而不是覆盖 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports</resource>
</transformer>
<!-- 同批还要保留这些 -->
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>更稳的选择是用 spring-boot-maven-plugin 打可执行 jar,它按 Boot 的布局处理这些资源。打包改动必须有一条验证:在镜像里跑一次 unzip -p,确认清单行数等于依赖里 starter 的个数(经验值,需校准)。
复盘要点:这类问题的共同形状是“装配结果依赖打包产物”,而打包产物只有 CI 会生成。把 --debug 的条件报告在流水线里落盘成构建产物,出问题时不必重跑镜像就能比对两个环境的差异。
二、配置改了不生效:优先级与前缀
现象:把 sms.thread-pool.size 从 4 改到 16 后,pool 监控里仍显示 4。改过 application.yml、也重启过服务;K8s 里 ConfigMap 已更新,env 里也看到一个同名变量。
排查:值不对,先确定“最终生效的那一份来自哪里”,而不是猜解析顺序。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 实际生效值 | curl /actuator/configprops,看该 @ConfigurationProperties 绑定后的字段 | 16 表示绑定成功,是代码用了别的值 |
| 值来源 | curl /actuator/env,按 key 搜,会显示命中的 PropertySource 名称与顺序 | 命中 systemEnvironment 而不是 ConfigMap 文件 |
| 前缀是否匹配 | @ConfigurationProperties(prefix = "sms.thread-pool") 与 yml 层级是否一致 | 前缀写错 → 绑定为默认值 |
| relaxed 规则 | 环境变量 SMS_THREADPOOL_SIZE 与 sms.thread-pool.size 是否被视为同一个 key | 大写+下划线形式会被规范化匹配 |
本例命中的是环境变量:ConfigMap 以文件方式提供 application.yml,同时另一个模板注入了 SMS_THREADPOOL_SIZE=4。Boot 的属性来源优先级里,操作系统环境变量高于打包在 jar 内的配置文件,所以文件里的 16 一直被压住。
对症做法:
bash
# 1. 先确认谁在覆盖谁,再动手
kubectl exec pod -- env | grep -i sms
curl -s localhost:8080/actuator/env | grep -A3 sms.thread-pool
# 2. 统一配置出口:容器环境变量只放“平台级”项,业务参数一律走配置中心
spring.config.import: optional:nacos:sms-service.yaml
# 3. 需要明确失败而不是静默用默认值时,加校验
@Validated
@ConfigurationProperties(prefix = "sms.thread-pool")
class SmsProperties { @Min(1) @Max(64) int size = 4; }排障时不要靠日志里打印的属性拼接,/actuator/env 会给出每个 key 的来源名,这是唯一可靠的判定证据。生产环境记得限制端点暴露范围,只对内网监控网段开放。
复盘要点:配置来源多于一个时,任何“改了没生效”都要先看来源命中。把 /actuator/configprops 的快照纳入发布校验(发布后自动 diff 关键项),能提前发现“改了但没落地”的参数;这类问题在压测调参时最容易被误判为代码问题。
三、启动 90 秒,探针把服务打回 CrashLoop
现象:网关服务启动耗时从 25 秒涨到 90 秒以上(经验值,需校准),K8s startupProbe 阈值 60 秒,连续失败后被 kubelet 杀掉,Pod 进入 CrashLoopBackOff;每次重启都会给注册中心带来一批瞬时注册/下线。
排查:先确定时间花在哪一步,再决定优化对象。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 启动阶段分布 | management.endpoint.startup.enabled=true,取 /actuator/startup 的 spring.boot.application.starting → ready 各 span 时长 | 某个 Bean span 占据大头 → 具体 Bean 初始化慢 |
| 候选类数量 | --debug 里 Positive matches 条数;对比同类服务的 starter 个数 | 明显偏多 → 引入了不需要的自动配置 |
| 是否卡在 Runner | 日志停在 Started ... in 之前,ApplicationRunner 里是否有预热/远程调用 | Runner 阻塞会推迟 finishRefresh 之后的就绪 |
| 端口何时打开 | 搜 Tomcat started on port | 出现在第 12 步,探针打到这一步才算通 |
本例是三类叠加:引入了 spring-boot-starter-* 全家桶(正例匹配 140 余项,其中近一半与该服务无关,经验值,需校准)、CommandLineRunner 里同步拉取一份 3 万条的字典、Actuator 与监控 Agent 在启动期做远程注册。
对症做法:
properties
# 1. 排除确定不需要的自动配置,而不是全局懒加载
spring.autoconfigure.exclude=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfigurationjava
// 2. 预热移出启动路径:就绪事件里异步跑,不占 refresh 的时间
@EventListener(ApplicationReadyEvent.class)
public void warmup() { executor.submit(this::loadDict); }
// 预热没完成就不该接流量的服务,再加一个自定义 readiness 指示器,
// 在 loadDict 完成后才置 UP,并归入 readiness 组
class WarmupHealthIndicator implements HealthIndicator { /* ... */ }yaml
# 3. readiness 组里带上预热指示器:没热完就不接流量
management.endpoint.health.group.readiness.readiness.include: ping,db,warmupyaml
# 4. 探针给启动留窗口,readiness 与 liveness 分开设
startupProbe: { httpGet: { path: /actuator/health/readiness, port: 8080 }, periodSeconds: 5, failureThreshold: 30 }
readinessProbe: { httpGet: { path: /actuator/health/readiness, port: 8080 }, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /actuator/health/liveness, port: 8080 }, periodSeconds: 10, failureThreshold: 6 }spring.main.lazy-initialization=true 能让启动数字变好看,但把 Bean 创建成本推给第一个请求,冷启动时 P99 会尖刺;它不是这类问题的解法。
复盘要点:启动时长应该当作指标管起来。给每个服务设一条启动预算,超过就在流水线告警;/actuator/startup 的输出按天留档,能直接看出是哪一次依赖引入把成本推上去的。注册中心侧配合 优雅上下线与启动检查,避免反复重启造成的瞬时抖动放大。
最小上手
看条件报告
bash
# 方式一:启动参数(Boot 3.x)
java -jar app.jar --debug
# 方式二:application.properties
debug=true输出里重点看 Positive matches(哪些自动配置生效)和 Negative matches(为什么没生效,通常会写 @ConditionalOnClass did not find required class 或 @ConditionalOnMissingBean found beans)。
用 Actuator 查
先配置 management.endpoints.web.exposure.include=conditions,beans,再执行:
bash
curl -s http://localhost:8080/actuator/conditions | head写一个自定义自动配置
java
@AutoConfiguration
@ConditionalOnClass(name = "com.example.sms.SmsClient")
@EnableConfigurationProperties(SmsProperties.class)
public class SmsAutoConfiguration {
@Bean
@ConditionalOnMissingBean
SmsClient smsClient(SmsProperties props) {
return new SmsClient(props.getEndpoint());
}
}在 src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 写入一行:
com.example.sms.SmsAutoConfiguration打包后引入该依赖,SmsClient 就会在条件满足时自动注册;用户自己声明 SmsClient 时,@ConditionalOnMissingBean 让默认实现退出。
生产动作
spring.autoconfigure.exclude=...(无默认值):排除不需要的自动配置,值是类的全限定名;也可以用@SpringBootApplication(exclude = ...)。spring.main.lazy-initialization=false(默认 false):打开会推迟 Bean 创建,启动更快但问题暴露更晚;排障期不要开。spring.main.allow-bean-definition-overriding=false(默认 false):Bean 名冲突会直接启动失败;不要为了“先跑起来”改成 true,先确认是谁覆盖了谁。- 排障路径:现象“某个 Bean 不存在” → 查
--debug的Negative matches→ 看是 classpath 缺类、属性不匹配,还是用户 Bean 已存在 → 再查 imports 文件路径和类名是否写错。 - 排障路径:现象“自动配置生效了但值不对” → 查
@ConfigurationProperties前缀是否匹配、是否被环境变量/命令行覆盖 → 用 Actuator/actuator/env和/actuator/configprops对照,不要靠猜。
边界
- 自动配置不是业务装配:它负责框架基础设施;业务 Bean 用
@Component/@Bean显式声明,不要写一个自动配置去扫描业务包。 @ConditionalOnMissingBean不要乱用在用户配置里:它的语义是“当前已处理的定义中没有该 Bean”,在用户配置里使用会受解析顺序影响;把它放在@AutoConfiguration类里。- 条件不是运行时判断:条件在启动期评估一次,运行期新增/删除 Bean 不会重新评估。
- 不要依赖
spring.factories写自动配置:Boot 3.x 已不再从该文件加载自动配置候选,迁移时把类名移到AutoConfiguration.imports。 - 自动配置会隐藏启动成本:引入 starter 就多一批候选类和条件评估;启动慢时先看条件报告和
startup端点,而不是盲目加lazy-initialization。
讲清楚
Spring Boot 自动装配的核心不是“魔法”,而是一份 classpath 里的候选清单加一组条件注解。启动时
AutoConfigurationImportSelector读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,在 refresh 的第 5 步解析这些配置类;每个@Bean再用@ConditionalOnClass、@ConditionalOnMissingBean等条件决定是否注册。用户自己的配置先解析,所以默认实现会让位。时序上容易记错的是 WebServer:它在
onRefresh()(第 9 步)创建,早于业务单例(第 11 步);但真正开始接受请求是在finishRefresh()(第 12 步)的 Lifecycle 启动阶段。自动配置本身也在第 5 步就完成了条件判断,不是在业务 Bean 创建之后。生产排查先看条件报告:
--debug或 Actuatorconditions会直接告诉你哪个条件没满足。不要用“排除所有自动配置再逐个加回来”的方式试错,那样很难定位真正的条件。