在我经手过的几个项目里,升级平台版本更多是为了主动偿还技术债,而不是追新。Java 21→25 和 Spring Boot 3.5→4.0 是我在跟踪的下一次升级,时机还没到,但提前把反复想到的三个问题梳理一遍通常能省不少麻烦:为什么升、升了能得到什么、怎么升。


版本节奏与维护周期#

在讨论「为什么升」之前,先把两个平台的发布规律和生命周期摆出来,后面的判断会有据可依。

Java#

Java 自 2017 年起切换到半年一版的节奏,每年 3 月和 9 月各发一个版本。其中每隔两年(从 Java 21 开始正式固定)发一个 LTS(Long-Term Support)版本,非 LTS 版本只在下一个版本 GA 后停止支持,实际窗口约 6 个月。

版本 类型 GA 时间 Oracle Premier 支持截止 Oracle Extended 支持截止
Java 17 LTS 2021-09 2026-09 2029-09
Java 21 LTS 2023-09 2028-09 2031-09
Java 25 LTS 2025-09 2030-09 2033-09
Java 26 非 LTS 2026-03 ~2026-09(Java 27 GA 时)
Java 29 LTS(预计) 2027-09

Oracle Premier Support 约 5 年,涵盖安全补丁和 bug 修复;Extended Support 额外 3 年,仅提供安全和关键补丁,通常需要付费授权。非 LTS 的 6 个月窗口不适合生产环境长期使用。

实践上,生产环境我只会跟 LTS 版本走:Java 17 → 21 → 25,下一站是 Java 29(2027-09 预计)。

Spring Boot#

Spring Boot 同样是半年一个 minor 版本,每年 5 月和 11 月发布。OSS 支持周期约 12 个月,大版本(major)至少维护 3 年;每个大版本系列的最后一个 minor(如 3.5.x)还会有额外的商业扩展支持窗口。

版本 GA 时间 OSS 支持截止 商业支持截止
3.3.x 2024-05 2025-06(已过) 2026-06
3.4.x 2024-11 2025-12(已过) 2026-12
3.5.x 2025-05 2026-06 2032-06(3.x 系列最后 minor)
4.0.x 2025-11 2026-12 2027-12
4.1.x ~2026-05(预计) ~2027-06

几个值得注意的点:

  • 3.5.x 是 3.x 的最后一个 minor,OSS 支持到 2026-06,商业扩展支持可以到 2032。如果近期没有计划升 4.x,在 3.x 系列里坚守 3.5.x 是最理性的选择。
  • 4.0.x OSS 支持到 2026-12,窗口不长。等 4.1 GA(预计 2026-05),从 4.0 升 4.1 是 minor 级别的轻量升级。
  • Boot 4.x 要求 Java 17+,Boot 4.0 建议搭配 Java 21 或 25。

为什么要升级#

技术债的复利效应#

不升级不代表没有成本,而是在以复利的方式积累风险:

  • 安全漏洞持续堆积,每延迟一个版本窗口,暴露面只增不减。
  • 依赖冲突越来越难解,为了绕过某个旧版本的 breaking change,dependencyManagement 里的手动 override 越堆越多,最终没人敢动。
  • 下一次升级代价指数级增长——从 Java 17 跳到 25,比逐步从 17→21→25 要痛苦得多。

对我来说,与其把升级当成额外成本,不如把它看成在还技术债的利息。

Spring Boot 3.5.x 的支持窗口正在收窄#

截至我写这篇时,Spring Boot 3.x 系列的 OSS 支持窗口已经在收窄。Boot 4.0 自 2025 年 11 月 GA 以来已经是主要活跃开发线,截至 2026-04 最新补丁版本是 4.0.6;4.1.x 也已经进入里程碑阶段(4.1.0-M4,2026-03 发布)。安全补丁、依赖对齐、生态系统重心都集中在 4.x 线,继续留在 3.5.x 等于主动选择一个缩短中的支持周期。


Java 21→25:语言层的成熟#

顺一句话:Java 26 已于 2026-03-17 GA,但它是非 LTS 版本。生产环境升级我目前仍把 Java 25(LTS,2025-09 发布)当目标,26 更适合在非生产环境跟进新特性。下一个 LTS 是 Java 29(预计 2027-09),意味着 Java 25 这条 LTS 的窗口会陪我相当一段时间。

模式匹配减少显式强转#

以前需要 instanceof 检查 + 手动 cast,现在语言层直接绑定变量:

// 旧写法
if (obj instanceof User) {
    User u = (User) obj;
    process(u);
}

// Java 21+ 写法
if (obj instanceof User u) {
    process(u);
}

在 Spring MVC 的 Controller 逻辑或消息处理器里,这类代码随处可见,减少了一类低级的 ClassCastException

Switch 表达式强制穷举#

switch 表达式配合密封类/枚举,编译器会强制要求覆盖所有分支,减少了业务逻辑里「悄悄漏掉一个 case」的可能:

return switch (status) {
    case CREATED  -> HttpStatus.CREATED;
    case UPDATED  -> HttpStatus.OK;
    case DELETED  -> HttpStatus.NO_CONTENT;
};

Record 成为默认数据载体#

从 Java 16 稳定以来,Record 的框架支持到 Java 25 已经相当成熟——Jackson 序列化/反序列化、Bean Validation 都能开箱即用。

对于 REST API 的 DTO 和消息队列的 payload,Record 的不可变性在我的使用经验里确实减少了多线程下状态共享出问题的频率;在不需要可变状态的场景里,它也算是我目前用过的成本比较低的方案之一。

虚拟线程(Project Loom)走向实用#

虚拟线程在 Java 21 正式 GA,经过 22→25 的持续打磨,在我接触到的几个 I/O 密集场景里已经能稳定使用。

对于 I/O 密集型 的 Spring 服务(外部 HTTP 调用、数据库查询、消息消费),虚拟线程能在不重写为响应式代码的前提下提升并发吞吐(具体提升幅度还是要以自己服务的压测为准):

// Spring Boot 3.2+ 配置虚拟线程执行器
@Bean
public TomcatProtocolHandlerCustomizer<?> virtualThreadCustomizer() {
    return handler -> handler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
}

在 I/O 密集场景里,CPU 利用率通常会有提升、线程阻塞开销也会下降,而代码形态依然是同步风格。

运行时可观测性增强#

  • GC 调优信号更精细
  • 内存诊断更清晰
  • Java Flight Recorder(JFR)事件更丰富
  • 异常栈帧信息更有用

这些改进在我排查问题时通常能省一些时间。


Spring Boot 3.5→4.0:框架层的几个重要改动#

依赖基线统一重置#

Boot 4 在内部把所有传递依赖的版本一起移动到新基线,旧的手动 override(用来规避某个冲突)大概率会失效。这看起来是「破坏」,实际上是把隐藏的版本漂移提前暴露出来——比在生产环境被奇怪的运行时行为发现,通常还是要好一些。

结果是:安全补丁更容易落地,依赖图更干净。

Starter 粒度细化#

Boot 4 将部分大粒度 starter 拆分为更小的模块,带来两个直接好处:

  1. 依赖树更短,启动时扫描的类更少,启动速度通常会有改善。
  2. 每个服务只引入真正需要的依赖,减少不必要的暴露面。

配置属性:尽量在启动期暴露错误#

Boot 4 清理了一批已经 deprecated 的配置 key,并将 binding 规则收紧。未知或错误的配置属性会在启动时直接抛错,而不是静默忽略后在生产环境出现诡异行为。

具体哪些 key 在 4.x 里被改名或移除,建议直接以官方 Spring Boot 4.0 Configuration Changelog 为准——这一类机械迁移恰好也是 OpenRewrite 的 SpringBootProperties_4_0 recipe 的主要工作。

这是有意为之的设计取舍:宁可让错误在开发阶段暴露,也不让它们悄悄流入生产。

可观测性成为默认假设#

Boot 4 默认假设你的服务有 Metrics、Tracing 和结构化日志,Actuator、Micrometer 的集成深度进一步提升。这与我接触到的云原生部署习惯比较一致——可观测性已经被当成基础设施的一部分,而不是可选项。


迁移策略#

策略一:手动逐步迁移#

适合规模不大、改动点可控的项目,核心原则是一次只动一个轴

第一步:锁定 Java 25#

<properties>
  <maven.compiler.source>25</maven.compiler.source>
  <maven.compiler.target>25</maven.compiler.target>
</properties>

配合 Maven Enforcer 插件强制约束,防止本地和 CI 环境编译版本不一致:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-enforcer-plugin</artifactId>
  <executions>
    <execution>
      <goals><goal>enforce</goal></goals>
      <configuration>
        <rules>
          <requireJavaVersion>
            <version>[25,)</version>
          </requireJavaVersion>
        </rules>
      </configuration>
    </execution>
  </executions>
</plugin>

第二步:升级 Spring Boot 版本#

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.0.6</version>
</parent>

第三步:按顺序修复失败#

我的经验是不要跳步,按以下顺序逐一处理:

  1. 编译错误:API 删除、包路径变更
  2. 依赖冲突:清理无效的 version override
  3. 测试失败:Mock 行为变化、序列化格式变化
  4. 启动失败:配置属性 key 变更
  5. 运行时行为:端到端验证

策略二:OpenRewrite 自动化迁移#

适合中大型代码库,改动点分散、机械重复的情况。OpenRewrite 在 AST(抽象语法树)层面做变换,不是简单的文本替换,保证变换的确定性和可预览性。

Java 25 迁移 Recipe#

pom.xml 中添加插件:

<plugin>
  <groupId>org.openrewrite.maven</groupId>
  <artifactId>rewrite-maven-plugin</artifactId>
  <version>6.39.0</version>
  <configuration>
    <activeRecipes>
      <recipe>org.openrewrite.java.migrate.UpgradeToJava25</recipe>
    </activeRecipes>
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.openrewrite.recipe</groupId>
      <artifactId>rewrite-migrate-java</artifactId>
      <version>latest.release</version>
    </dependency>
  </dependencies>
</plugin>

UpgradeToJava25 Recipe 会自动处理:

  • 构建配置对齐到 Java 25
  • 废弃/删除 API 的替换
  • 旧版安全相关写法的现代化改写
  • 语言特性与 I/O API 的升级

执行流程(不可跳过 dry run):

# 先预览,审查 diff
mvn rewrite:dryRun

# 确认无误后执行
mvn rewrite:run

# 验证编译和测试
mvn clean compile
mvn test

Spring Boot 4 迁移 Recipe#

同样的插件结构,替换 Recipe 名称和依赖:

<activeRecipes>
  <recipe>org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0</recipe>
</activeRecipes>
<dependencies>
  <dependency>
    <groupId>org.openrewrite.recipe</groupId>
    <artifactId>rewrite-spring</artifactId>
    <version>latest.release</version>
  </dependency>
</dependencies>

UpgradeSpringBoot_4_0 Recipe 覆盖:

  • Maven/Gradle 构建文件的版本对齐
  • 配置属性 key 自动重命名(避免启动崩溃)
  • 细粒度 starter 替换
  • 废弃 API 的批量清理
mvn rewrite:dryRun  # 先 review diff
mvn rewrite:run
mvn clean compile
mvn test

OpenRewrite 能保证什么,不能保证什么#

能保证 不能保证
已知迁移点的一致性应用 运行时性能表现
废弃写法的完整清理 并发正确性
可预览、可审查的 diff 负载下的内存行为
变换的可重复性 业务逻辑的正确性

OpenRewrite 修的是结构和语法,行为验证仍然是人的责任


架构层面的建议#

两轴独立,分步推进#

我习惯把两者拆开,不捆绑在同一个 PR 里。这样每次改动的风险边界更清晰,出了问题也比较容易判断是平台层还是框架层的问题。

我倾向的推进顺序:

Java 21 → Java 25(编译通过,测试绿)
    ↓
Spring Boot 3.5 → Spring Boot 4.0(启动成功)
    ↓
运行 Spring Boot 4 OpenRewrite Recipe
    ↓
重新跑完整测试套件
    ↓
运行时行为验证(对比指标)
    ↓
Canary 发布 → 全量上线

没有指标,升级就是猜测#

我习惯在平台升级时一起准备量化对比指标,否则很难回答「有没有变坏」:

  • 错误率
  • P95/P99 延迟
  • 内存占用与 GC 行为
  • 下游依赖的失败率

如果还回答不了「升级前后行为有没有变化」,我一般不会把升级算成完成。

小步快跑,无聊即成功#

在我经历过的升级里,最顺的那些反而是最无聊的——变更集小、diff 清晰、测试绿、悄悄上线、没人察觉。

在我的观察里,每次及时升级确实让下一次变得轻松一些;这也是我倾向于保持版本不落太远的主要原因。反之,积累越久,下次的代价往往越高。


小结#

Java 21→25 和 Spring Boot 3.5→4.0 的升级本质上是一次平台健康维护,而非功能开发。拖延的代价是以复利计算的技术债,升级的收益是更低的运维风险、更好的语言工具和更长的安全支持窗口。

落地路径上,小项目走手动逐步迁移,大项目用 OpenRewrite 自动化处理机械变更,两种策略都需要严格的测试验证和分阶段发布。核心原则始终是:一次一个关注点,先验证再推进