跳到正文

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),但不再用于自动配置。

条件注解与顺序 ​

注解判断典型用法
@ConditionalOnClassclasspath 是否有指定类有 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.HibernateJpaAutoConfiguration
java
// 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,warmup
yaml
# 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 或 Actuator conditions 会直接告诉你哪个条件没满足。不要用“排除所有自动配置再逐个加回来”的方式试错,那样很难定位真正的条件。

把一门技术讲透到既懂也会用