本文来自一次真实升级。服务名、项目坐标、Pod、节点、镜像、命名空间和组织信息均已脱敏或泛化。本文关注升级排查方法,请勿未经验证就将配置或修复代码复制到其他服务。
最近将两个 Spring Boot 服务从 3.1.7 升级到 3.5.13,同时将运行时从 Java 17 切换到 Java 25。Maven 编译和打包均能通过,但部署到测试 Kubernetes 集群后,两个服务先后出现启动失败、完整健康检查异常。
这次升级最值得记录的地方在于:问题都不在业务代码的语法层面,而在于框架默认行为发生了变化。下面按实际排查顺序整理。
本文复盘已经完成的 3.1.7 → 3.5.13 升级。下一篇从 Spring Boot 3.5.13 升级到 4.1只讨论下一阶段新增的兼容边界,不再重复本文的通用启动、健康检查和中间件验收流程。
先建立升级判断链路
flowchart LR
A["升级 Spring Boot 与 JDK"] --> B["目标 JDK 编译与打包"]
B --> C["新 Pod 启动日志"]
C --> D["完整 /actuator/health"]
D --> E["中间件命令与协议验证"]
E --> F["滚动发布与 Endpoint 切换"]
升级失败时,不能因为报错日志里出现了某个组件,就直接跳到根因。先区分故障发生在 Bean 创建、框架初始化、健康检查,还是业务请求,再为每一层补充独立证据。
升级范围与验收边界
父 POM 的 Spring Boot 版本从 3.1.7 升级到 3.5.13,JDK 从 17 升级到 25。
升级前先确认两件事:
- CI、容器基础镜像和本地 Maven 使用的是同一代 JDK;
- 不能只以“编译通过”为验收标准,至少还要验证应用启动、完整 Actuator 健康检查和关键中间件连接。
本次的三个问题都属于第二类:它们不阻塞编译,却会在新副本启动或实际探测时暴露。
问题一:Spring Batch 的 Job 名称重复,应用无法启动
现象:新副本在注册 Job 时失败
其中一个服务启动时抛出类似异常:
DuplicateJobException: A job configuration with this name [initPriceJob] was already registered
代码中两个不同的 @Bean 方法分别声明了 Job,但其中一个 JobBuilder 的内部名称仍然是 initPriceJob。
@Bean
public Job productionInitPriceJob(...) {
return new JobBuilder("initPriceJob", jobRepository)
// ...
.build();
}
证据与根因:Bean 名称并不等于 Batch Job 名称
Spring Batch 升级后,默认配置会在容器初始化阶段收集所有 Job Bean 并注册到 JobRegistry。因此,过去未暴露的“内部 Job 名称重复”问题,在新版本启动阶段变成了确定性失败。
这里要区分两个名字:
- Spring Bean 名称:例如
productionInitPriceJob; - Spring Batch Job 名称:
new JobBuilder("...")中传入的名字。
真正参与 JobRegistry 去重的是后者。
修复:让 JobBuilder 名称唯一
将 JobBuilder 的内部名称改为唯一,并与业务 Job 对齐:
return new JobBuilder("productionInitPriceJob", jobRepository)
// ...
.build();
验证与经验
升级 Spring Batch 时,建议对所有 new JobBuilder(...) 做一次全局扫描,检查字符串名称是否重复,而不是只检查 @Bean 方法名。
问题二:TaskExecutor 从“按名字注入”变成“两个候选 Bean”
现象:两个候选 Bean 导致注入失败
另一个服务启动失败:
Parameter 0 of ... required a single bean, but 2 were found:
- applicationTaskExecutor
- taskScheduler
原代码是字段注入:
@Autowired
private TaskExecutor taskExecutor;
服务同时启用了异步和定时任务。升级后,容器中既存在 applicationTaskExecutor,也存在 taskScheduler;两者都可以匹配 TaskExecutor,Spring 无法再猜测应该注入哪一个。
证据与根因:曾经的隐式名称匹配不再成立
在旧版自动配置中,默认异步执行器还会以 taskExecutor 的名称暴露。字段名恰好与这个别名一致时,Spring 可以利用名称匹配完成注入。
新版自动配置保留了 applicationTaskExecutor,但不再以同样方式提供 taskExecutor 别名;同时启用调度功能后,taskScheduler 也会成为 TaskExecutor 候选 Bean,原来依赖字段名的隐式行为随之失效。
修复:把线程池选择写进代码
明确依赖的是应用异步线程池:
@Autowired
@Qualifier("applicationTaskExecutor")
private TaskExecutor taskExecutor;
如果项目中存在多个相同类型的注入点,建议全部搜索并逐一修复,例如:
rg 'TaskExecutor taskExecutor' src/main/java
验证与经验
不要让业务代码依赖 Spring Boot 自动配置 Bean 的历史别名。对于线程池、数据源、缓存管理器这类容易出现多个候选 Bean 的类型,应使用 @Qualifier,或者改为构造器注入并在参数上标注限定名。
问题三:MongoDB 看似“连不上”,实际是健康检查命令不兼容
现象:MongoDB 看似“连不上”
应用启动后,日志持续出现:
MongoDB health check failed
Command failed with error 59 (CommandNotFound):
no such command: 'hello'
第一反应往往集中在 DNS、Service、账号密码或网络策略上。但在集群内验证后发现:
db.adminCommand({ ping: 1 });
// { "ok" : 1 }
db.adminCommand({ isMaster: 1 });
// { "ismaster" : true, ..., "ok" : 1 }
db.adminCommand({ hello: 1 });
// no such command: 'hello'
测试环境的 MongoDB 是 4.2.8;Mongo Java Driver 本身已经成功建立连接。失败发生在完整 /actuator/health 调用默认的 MongoHealthIndicator 时。
证据与根因:失败的是 Actuator 命令,不是 TCP 连接
Spring Boot 3.1 的 Mongo 健康检查执行的是:
mongoTemplate.executeCommand("{ isMaster: 1 }");
Spring Boot 3.5 改为了:
mongoTemplate.executeCommand("{ hello: 1 }");
MongoDB 4.2 不认识 hello 命令,所以健康检查为 DOWN。这里的重点是:健康检查失败不等于应用无法建立 Mongo 连接。先结合 ping、驱动连接日志和业务读写来验证,才能判断是协议、认证还是网络问题。
修复:保留真实检查,覆盖不兼容的默认命令
不建议为了让健康检查变绿,直接关闭 Mongo 健康检查:
management.health.mongo.enabled=false
这样会丢失真实的 Mongo 可用性信号。
本次采用的是保留健康检查语义、覆盖默认贡献者的方式:用与默认贡献者相同的 Bean 名称,提供一个兼容 MongoDB 4.2 的检查实现。
@Configuration(proxyBeanMethods = false)
public class MongoHealthConfiguration {
@Bean(name = "mongoHealthContributor")
public HealthIndicator mongoHealthContributor(MongoTemplate mongoTemplate) {
return new AbstractHealthIndicator("MongoDB health check failed") {
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
Document result = mongoTemplate.executeCommand("{ isMaster: 1 }");
builder.up().withDetail("maxWireVersion", result.getInteger("maxWireVersion"));
}
};
}
}
Spring Boot 的自动配置在发现名为 mongoHealthContributor 的 Bean 后,会跳过默认实现。最终 /actuator/health 仍会真实检查 Mongo,且能兼容本地 4.2 实例。
验证与经验
升级框架时,Actuator 也属于运行时兼容面。特别是数据库、消息队列、注册中心的健康检查命令,可能比应用业务代码更早碰到旧服务端兼容性问题。
长期方案仍然是规划 MongoDB 服务端升级;应用侧兼容实现适合在测试环境或迁移过渡期使用。
第 4 步:将验证写成升级验收清单
仅执行 mvn package 不足以证明升级成功。对于 Spring Boot 大版本或跨多个小版本的升级,建议按下面顺序验收:
- 使用目标 JDK 执行编译和打包;
- 在本地或测试环境启动新副本,确认没有 Bean 注入、自动配置或 Job 注册异常;
- 请求完整
/actuator/health,不要只看 Kubernetes 的 liveness/readiness; - 核验 DB、Redis、RabbitMQ、MongoDB、Nacos 等关键组件均为
UP; - 对 Job 名称、线程池注入、序列化配置和中间件协议命令做针对性回归;
- 滚动发布后确认新 Pod Ready、Endpoint 已切换、旧 Pod 正常退出。
总结:显式化原本隐含的框架契约
这次从 Spring Boot 3.1.7 到 3.5.13 的升级,没有遇到传统意义上的 API 编译错误,却暴露了三类更隐蔽的问题:
| 类型 | 根因 | 修复原则 |
|---|---|---|
| Spring Batch | 新版启动时严格注册全部 Job,暴露内部名称重复 | 保证 JobBuilder 名称全局唯一 |
| 异步线程池 | 自动配置 Bean 别名变化,出现多个 TaskExecutor 候选 | 显式 @Qualifier,不依赖字段名匹配 |
| Mongo 健康检查 | 默认命令从 isMaster 改为 hello,旧 Mongo 不支持 | 覆盖健康检查并保留真实连通性检测 |
框架升级的核心不是“把版本号改上去”,而是识别并验证那些原先依赖默认行为、但从未被显式写进业务代码的契约。
如果升级后的问题出现在容器目录、废弃属性、commons-logging、Nacos Logback 或可选 JDBC 驱动,可统一参见Spring Boot 3 容器启动异常复盘。
DISCUSSION
讨论与反馈