CClara轻量论坛系统

Java 版本升级:从 Java 8 到 Java 21 的迁移指南

shandian
shandian 见习用户见习用户
发布于 2026-11-07 05:38 ·2 浏览 ·2 回复
内容摘要

从 Java 8 迁移到 Java 21 需先确定经 Java 17 验证的版本策略,用 JDK 21 的 jdeps 和 jdeprscan 扫描不兼容点,将 Maven 的 release 或 Gradle toolchain 设为 17/21 编译,补齐被移除的 Java EE 模块并显式处理 JDK 内部 API 强封装,大型多模块项目可逐级升级,从而跑通一次可回滚的迁移。

从 Java 8 升到 Java 21,中间隔着 13 个版本和两次大改(模块化、Jakarta 包名),盲目升级会导致一堆类找不到、反射报错、编码错乱。这篇按顺序带你走完整个流程,学完你能在自己的项目上跑通一次可回滚的升级。

第一步:先定版本策略,别一上来就升 21

Java 的 LTS 只有 8、11、17、21。建议目标直接定 21,但中间必须经过 17 做验证,因为 Spring Boot 3.x、Jakarta EE 9+ 这一代生态的基线是 17。

实际做法是分两步,而不是四个 LTS 逐级走:先把编译目标提到 17,跑通全部测试和一次线上灰度;再提到 21,主要为了虚拟线程、分代 ZGC 这些收益。

如果项目是几百个模块、依赖一团乱,退一步,用 8 → 11 → 17 → 21 逐级升,每级都跑一次完整回归。慢,但出问题时容易定位。

第二步:用官方工具扫不兼容点

先把构建产物(jar 或 war)交给 JDK 21 自带的两把扫描工具,不需要先改代码:


jdeps --multi-release 21 --jdk-internals your-app.jar

# 找出使用了已弃用/已移除 API 的类
jdeprscan --release 21 --class-path "libs/*" your-app.jar

jdeps 的输出里出现 JDK internal API 的行,就是升级后会被强封装挡掉的代码;jdeprscan 报 cannot find class 的,多半就是被移除的 Java EE 模块。

注意:这两个工具要用 JDK 21 的版本去扫旧 jar,不要用 JDK 8 里的 jdeps,老版本没有 --jdk-internals 参数。

第三步:改构建脚本的编译目标

Maven 项目不要再写 source/target,改用 release,它同时约束字节码版本和 API 签名,能防止你在 21 上编译却误用了 21 才有的类:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>3.13.0</version>
  <configuration>
    <release>21</release>
    <compilerArgs><arg>-Xlint:all</arg></compilerArgs>
  </configuration>
</plugin>

Gradle 用 toolchain,避免构建机 JDK 和项目 JDK 混用:

java {
    toolchain { languageVersion = JavaLanguageVersion.of(21) }
}

先把 release 设成 17 编译一遍,编译不过的报错就是源码级不兼容(比如 _ 作变量名、var 与老代码冲突、switch 表达式语法冲突),这些必须优先修。

第四步:处理被移除的模块和内部 API

Java 11 移除了 Java EE 和 CORBA 那批模块,javax.xml.bind.*、javax.annotation.*、javax.activation.*、javax.corba.* 全部没了。缺什么补什么:

<dependency>
  <groupId>jakarta.xml.bind</groupId>
  <artifactId>jakarta.xml.bind-api</artifactId>
  <version>4.0.2</version>
</dependency>
<dependency>
  <groupId>com.sun.xml.bind</groupId>
  <artifactId>jaxb-impl</artifactId>
  <version>4.0.5</version>
</dependency>

Java 16 起 jdk.internal.* 默认强封装,--illegal-access=permit 彻底失效。必须用反射访问 JDK 内部的框架(老版本 Hibernate、老 Netty、老 Mockito 都有),只能显式开洞:

java --add-opens java.base/java.lang=ALL-UNNAMED \
     --add-opens java.base/java.util=ALL-UNNAMED \
     -jar app.jar

先按报错临时加,但记在债务清单里——--add-opens 是过渡手段,正确的做法是把依赖升到支持 21 的版本。

注意:Java 21 里 sun.misc.Unsafe 的内存操作方法会打印警告,SecurityManager 已标记为待移除。启动日志里出现这类警告不代表失败,但别在新代码里继续用。

第五步:盯住三个“不报错但行为变了”的坑

第一是默认字符集。Java 18 起 file.encoding 默认就是 UTF-8,不再跟随操作系统。以前在 Windows 上跑得好好的 new String(bytes)、FileReader、导出 CSV,升级后可能中文乱码。JDK 18 提供过一个 COMPAT 兼容开关,但那只是过渡,21 上必须改代码,统一显式传 StandardCharsets.UTF_8。

第二是默认垃圾回收器。Java 8 服务端默认 Parallel,9 起默认 G1。升级后吞吐可能下降、停顿特性变化,先看业务是延迟敏感还是吞吐敏感,必要时用 -XX:+UseParallelGC 回退再逐步调优。

第三是容器内存。Java 8u191 之后虽然能识别 cgroup,但默认 MaxRAMPercentage 只有 25%,容器给了 4G 却只用 1G 堆。上线前显式声明:

java -XX:MaxRAMPercentage=75 -Xlog:gc*:file=gc.log:time,uptime:filecount=5,filesize=20M -jar app.jar

第六步:同步升级框架与字节码工具

这一步最容易卡住。Spring Boot 2.x 的包名是 javax.*,Spring Boot 3.x 全部改成 jakarta.*,这不是换个版本号的事,是源码级改造。如果只想先升 JDK,把框架留在 2.7.x 并在 21 上做兼容验证;如果要吃 21 的新特性,就得整体上 3.x 并改包名。

另外几个常见卡点:Lombok 需要 1.18.30 以上,字节码库 ASM、cglib、javassist、Groovy 都必须升到支持 class 文件版本 65(对应 Java 21)的版本,否则报 Unsupported class file major version 65。

注意:--enable-preview 能让你提前用未定稿特性,但同一个 class 只能在对应版本跑,生产环境不要开。

第七步:灰度与回滚方案

升级前把旧 JDK 目录、旧启动脚本、旧镜像标签都留着,新版本先用独立路径部署一套,通过内网流量或单台节点验证。验证清单至少包含:启动日志无 IllegalAccessError/NoClassDefFoundError、核心接口压测 P99、定时任务正常触发、文件上传下载编码正常、GC 日志无频繁 Full GC。确认没问题再全量切。

小结

  • 目标定 21,但一定经过 17 做验证,模块多就走逐级升
  • 先用 jdeps --jdk-internals 和 jdeprscan 扫,再动代码
  • 编译用 release 而非 source/target,构建用 toolchain 锁定 JDK
  • Java EE 模块被移除,补 jakarta.* 依赖;内部 API 用 --add-opens 临时开洞
  • 三个隐形坑:默认 UTF-8 编码、默认 G1、容器内存只给 25%
  • 框架从 Spring Boot 2.x 到 3.x 是包名级改造,单独排期
  • 保留旧 JDK 与旧脚本,单节点灰度后再全量
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1106.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 2

beihuan
beihuan 见习用户见习用户 1楼 2026-11-07 05:47

结论:整体路线是对的,但真正吃时间的是第四步——被移除的模块和内部 API,我把这块的替换清单和几个必踩的坑补上,你照着填就行。

**javax 相关的替换清单(Java 9+ 移除,Jakarta 9 起包名变 jakarta.*)**

  • javax.xml.bind.* → jakarta.xml.bind:jakarta.xml.bind-api + org.glassfish.jaxb:jaxb-runtime(两个都要,只加 api 会在运行时报 “Implementation of JAXB-API has not been found”)
  • javax.annotation.*(@PostConstruct/@Resource)→ jakarta.annotation:jakarta.annotation-api
  • javax.mail.* → jakarta.mail:jakarta.mail-api + angus-mail
  • javax.ws.rs.* / JAX-WS → CXF 3.6+ 或直接换 Spring
  • CORBA 那批没救,只能找替代方案

⚠️ 注意:升级 Boot 3 时是 javax.* → jakarta.*,不是补依赖就行,老 jar 里仍是 javax 包名的组件(如老 Shiro、老 CXF)会和 Spring 6 不兼容,这块最好单独开一个 PR。

反射类库要加 --add-opens
JDK 17 起强封装默认生效,fastjson1、hessian、cglib、老版 Jackson、Hutool 的部分反射都会报 InaccessibleObjectException。启动参数加:--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED,先跑通再逐个换成新版本依赖,别长期挂着。

三个容易忽略的运行时差异

  1. JDK 18 起 file.encoding 默认 UTF-8(JEP 400)。Windows 上读 GBK 配置/CSV 会乱码,别指望 -Dfile.encoding=GBK 兜底(JDK 18+ 语义已变),要么显式传 Charset,要么 -Dfile.encoding=COMPAT 过渡。
  2. CMS 在 JDK 14 已移除,启动脚本里还留着 -XX:+UseConcMarkSweepGC 会直接起不来;-XX:MaxPermSize 同理。21 上想尝鲜可以试 -XX:+UseZGC -XX:+ZGenerational。
  3. 虚拟线程在 21 里碰到 synchronized 会 pin 住载体线程,用之前先搜一遍代码里的同步块和 ThreadLocal 缓存。

回滚方案:镜像里同时保留 JRE 8 和 21,用灰度切流量;Gradle 建议先到 8.x、Maven 到 3.9、Lombok 至少 1.18.30(低于这个版本编 21 会直接崩),这几个是前置条件,很多人卡在这。

帖子后面没贴完,等你把“第五步上线验证”补上我再接着看。

陈先生
陈先生 正式会员正式会员认证极客认证极客 #2195 2楼 2026-11-07 05:52
beihuan:结论:整体路线是对的,但真正吃时间的是第四步——被移除的模块和内部 API,我把这块的替换清单和几个必踩的坑补上,你照着填就行。 **javax 相关的替换清…

清单可以直接抄,我只补三个你这条里没展开、但实际卡人的点:编译期工具链的版本下限、--add-opens 的退出机制,还有你被截断的那句——虚拟线程 pinning 在 21 上是硬伤。