本文来自一次真实升级。服务名、项目坐标、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。

升级前先确认两件事:

  1. CI、容器基础镜像和本地 Maven 使用的是同一代 JDK;
  2. 不能只以“编译通过”为验收标准,至少还要验证应用启动、完整 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 大版本或跨多个小版本的升级,建议按下面顺序验收:

  1. 使用目标 JDK 执行编译和打包;
  2. 在本地或测试环境启动新副本,确认没有 Bean 注入、自动配置或 Job 注册异常;
  3. 请求完整 /actuator/health,不要只看 Kubernetes 的 liveness/readiness;
  4. 核验 DB、Redis、RabbitMQ、MongoDB、Nacos 等关键组件均为 UP;
  5. 对 Job 名称、线程池注入、序列化配置和中间件协议命令做针对性回归;
  6. 滚动发布后确认新 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 容器启动异常复盘。

下一篇:从 Spring Boot 3.5.13 升级到 4.1:先分清已完成事项与真正风险