从 Java 8 升级到 Java 23,踩坑记录、变更评估方法、辅助工具介绍
为什么要升级#
额,好比 iPhone 4S 很优秀,但是在 2025 年,我不会考虑买,甚至不考虑 iPhone 14 Pro Max。
当然后面我们会对比收益。
破坏性变更评估工具#
在升级之前,可通过 jdeps 和 jdeprscan 先评估下是否有使用内部类和废弃 API,有一个总的概览。
jdeps#
jdeps 是 Java 自带的命令行工具,可以用来分析依赖关系和生成模块信息文件,这里我们只借用他的其中一项功能。
通过 jdeps --jdk-internals
检查是否有使用内部 API,以下例子显示使用了 sun.net.util.IPAddressUtil
这个 Java 内部工具类,会显示详细的源码类和 jar 包位置。
可以继续使用 Java 中的内部 API,另外 OpenJDK Wiki 页面 Java Dependency Analysis Tool
推荐了某些常用 JDK 内部 API 的替换项,可参考这些建议替换掉。
jdeprscan#
jdeprscan 也是 Java 自带分析工具,可查看是否使用了已弃用或已删除的 API。使用已弃用的 API 不是阻塞性问题,还能接着跑,但是建议替换掉。使用已删除的 API,那就彻底跑不起来了。
扫描自己的代码中是否有使用废弃 API:
以上例子,com.company.Util 类在调用 java.lang.Double 类的已弃用构造函数。 javadoc 会建议用来代替已弃用 API 的 API。 但是无法解决“error: cannot find class sun/misc/BASE64Encoder”问题,因为它是已删除的 API, 自 Java 8 发布以来,应使用 java.util.Base64。
注意使用 jdeprscan 需要通过 –class-path 指定依赖项,可先执行 mvn dependency:copy-dependencies
命令,此时会 copy 依赖项到 target/dependency
目录。
如果你加了 –class-path 依赖,大概率还是报错误 error: cannot find class
,目测寻找依赖项是根据 target/dependency
中的名字顺序找的,不是常规的 java 进程一次性加载完,不知道这算不算 jdeprscan 的设计 Bug,具体解决办法可参考 jdeprscan-throws-cannot-find-class-error
。
我采用的解决方式是把所有依赖 jar 文件都接到某个文件夹,解压成 classes,然后 class-path 使用此文件。这样还有个好处,扫描报告里只有我真正要扫描的当前项目的报告,不包含第三方 jar 的。
以上过程命令行参考:
除了使用 jdeprscan 命令行以外,也可在你项目的 maven pom 文件中引入 maven-jdeps-plugin
,如下示例,引入后如果有使用废弃 API,将在 mvn package
的时候直接失败报错,避免有人无意引入废弃 API。
升级兼容方法#
利用 Maven 的 profile
机制,根据 JDK 版本号,自动激活不同的配置。
Java 模块化兼容。
你一定见过这种错误。
也一定知道怎么解决了,将没开放的模块强制对外开放,有两个参数选项:
–add-exports 导出包,意味着其中的所有公共类型和成员都可以在编译和运行时访问。
–add-opens 打开包,意味着其中的所有类型和成员(不仅是公共类型)都可以在运行时访问。
两者的区别在于 –add-opens 开放的更加彻底,不仅 public 类型、变量及方法可以访问,就连非 public 元素,也可以通过调用 setAccessible(true) 后也可以访问。简单起见,直接使用 –add-opens 即可。
使用 Maven 命令时,配置 maven-surefire-plugin 插件,参考如下:
在 IntelliJ IDEA 运行程序如果报错,可以通过在 “VM Option” 配置项中,增加 Java 模块化 --add-opens
相关启动参数即可正常启动。
完整 add-opens
列表。
推荐配置#
升级到 Java 21 以后以下是根据我们公司常规经验推荐的配置,非普世可用,请根据自己的应用情况臻选。
- 如果在使用 ZGC,推荐启用分代
-XX:+ZGenerational
,对稳定性、吞吐量、内存占用都有很大优化。Java 23 默认已启用分代 ZGC。 - 在很多场景下 G1 仍然是最稳的选择,内存占用比 ZGC 低,CPU 更稳定。大部分场景下小内存应用,并不需要 ZGC。
- 亲测大部分应用 Java 23 比 Java 21 内存占用约少 5%-10%,GC 更稳定。
辅助迁移工具#
一些辅助迁移到新版本的工具,仅供参考。
Maven 依赖包更新#
使用 maven 检查是否有最新的依赖包和插件,注意,这里给的建议仅供参考,不要一下子应用上,需要综合考虑当前的项目依赖关系。
EMT4J 也是一个静态分析工具,可输出分析报告,也可直接 apply 到 git,直接通过 maven 插件、cli 命令行、Java Agent 3 种方式分析。
目前发布比较慢,只有 master 分支支持 Java 21,可以基于 master 分支自己编译构建,也可以使用已 Realease 版本只分析到 Java 17。
可通过 emt4j-maven-plugin 进行检查。增加以下 plugin,执行 mvn emt4j:check
成功后查看报告。
注意 emt4j-maven-plugin 目前版本 0.8.0 比较老,请使用 Java 8 或 Java 17 跑 mvn 命令,版本太高会失败。
不想使用 xml 配置的,可参考以下命令行直接 run plugin。
检查结果错误可能很多,根据优先级修改,比如我的检查结果。
一键升级依赖包,重构源码,入门指导可参考我的另一篇博客:智能代码重构
。 OpenRewrite 更成熟易用。
检查 Java 命令行选项参数有没有问题,识别出已经过时不支持的参数。
Java 参数太多,到 VM Options Explorer - Corretto JDK21
中参照,里面根据 JDK 的版本以及发行商,列出来所有的相关参数,选择好对应发行商的正确版本,就可以搜索或者查看 java 命令支持的所有参数了。
遇见问题和解决办法#
一定要升级依赖包吗,不升级能编译通过,直接用 Java 21 能不能跑起来,会不会有问题。
以我们实际经验来看,确实有很多应该不升级可直接运行,也没有问题。但是遇到了一些应用,跑着跑着 OOM 了,切换回 Java 8 就没有问题,还不知道是哪个包不兼容。
TLS 不兼容问题,类似如下错误。JDK 17 是支持 TLS1.0 ~ TLS1.3 的,但是默认使用的 TLS 版本是 TLS 1.3, 老版本被禁用了,需要主动放开。
module jdk.proxy3 does not “opens jdk.proxy3” to unnamed module.
网上包括人工智能推荐的答案都是 add-opens,这也是我想到的第一个方式,毕竟以前遇见 unnamed module 都是这么干的。
实际上都不生效。Java 里并没有一个真的叫 jdk.proxy3
的模块,这是由 Dynamic Proxy 动态生成的一个虚拟方法。最根本的解决办法还是升级代码,不要调用 Java 过时的方法。我这里是因为 Groovy 调用产生,升级了一下 Groovy,完美解决。
参考资料#