文章目录
前言
我司Java应用有10多个,凡是涉及到基础架构及其配置的升级,例如:操作系统,Apache,Tomcat,pom.xml中的第三方依赖,以及JDK等。都要进行全平台统一升级和维护。所以一些看似简单的工作,由于涉及的Java应用之多,代码之复杂,反而需要我们更加细心、谨慎。
我司于去年将JDK从JDK8升级到JDK11,今年计划将JDK从JDK11升级到JDK17。升级到JDK17的任务由我负责,以下为升级过程中的一些思考和做法。该思路不局限于JDK11升级到JDK17,之后的JDK升级也同样适用。
如果你们公司现在也面临将JDK8升级到JDK11或JDK17的情况,推荐阅读京东技术团队关于升级JDK的文章。总的来说,我们升级过程中遇到的问题和他们遇到的非常像,相似度在80%。可能看完他们的文章,都不需要读我的文章了。
应该使用哪个供应商的JDK?
之所以有这个问题是因为Java被Oracle收购之后,Oracle JDK 8u202版本之后的JDK开始收费。如果你们公司现在用的还是Oracle JDK 8u202之前的版本,那么在升级时势必要面临抉择。
目前市面上JDK的供应商很多,有Oracle,RedHat,还有开源的Adoptium Eclipse Temurin。这些JDK之前到底有什么不同?建议阅读一个韩国工程师写的 Which Version of JDK Should I Use?,有助于各位了解目前JDK的供应商和各个版本的JDK的不同。
由于我们不考虑商用,以开源的优先,所以我们选择Adoptium Eclipse Temurin,并根据其Release Roadmap以及实际情况来选择进行后续的升级。
JDK17到底快了多少?
上面京东技术团队的文章中有简要描述,这里再出几个文章链接
有关GC的几篇文章
升级思路
阅读官方升级指南
仔细阅读Oracle官方的升级到JDK17的迁移指南。虽然我们使用的是开源的JDK而不是付费的Oralce JDK,但对于一些JDK标准的变动,二者是保持一致的。
升级到JDK17后一些代码不兼容的问题
我们碰到了一个关于空指针异常消息判断的不兼容
在JDK11中
public static int parseInt(String s, int radix)
throws NumberFormatException
{
if (s == null) {
throw new NumberFormatException("null");
}
}
在JDK17中
public static int parseInt(String s, int radix)
throws NumberFormatException
{
if (s == null) {
throw new NumberFormatException("Cannot parse null string");
}
}
有些开发人员,在处理异常时根据异常的字符串进行了判断,此时升级到JDK17,此判断无效,导致代码出现bug。
Fix: 修改判断空指针的逻辑,不允许根据异常消息的字符串进行判断
升级到JDK17后如何确保代码中的反射可以正常调用?
在阅读Oracle JDK17升级指南时,我发现了一段描述
对于还在使用JDK8的同学来说,要理解这段话,需要了解一个小背景。在JDK9开始,JDK推出了Module System的概念,即对JDK 的包进行了更细致的切分,并对代码反射的访问权限进一步严格限制。
不了解Module System的可以看一下京东技术团队 JDK8升级到JDK11有关模块化的说明或自行Google相关知识。
JDK9-15虽然已经采用模块化,但是对于一些代码反射的访问权限并没有限制很严格,用户依然可以通过添加 --illegal-access=permit
来使得反射可以顺利进行。
从JDK16开始默认禁止所有非法反射访问 --illegal-access=deny
(即如果想要通过反射访问某个包,该包在模块文件module-info中必须使用opens
指令声明)。但是JDK16中还是可以修改为permit
从JDK17开始–illegal-access将不再有效(见JEP 403),JVM启动时会忽略该选项。
找出哪些第三方库无法执行反射
了解了上述背景知识之后,再来看实际遇到的问题。这么多Java应用都在使用JDK11,代码之多,使用的第三方依赖之多,如何找出来哪些代码的反射是在JDK17中不被允许的呢?
- 为JVM添加启动参数
--illegal-access=warn
并将该参数应用到所有Java应用的测试以及产品环境中,测试环境每天有大量的auto smoke test帮我们做测试。 定期分析测试环境和产品环境日志,看日志中是否有类似日志WARNING: Illegal reflective access by
- 步骤1 应用到产品环境一段时间后,如果有上述警告日志,则根据实际情况添加–add-opens参数,例如
也可以不加=号,JDK官方关于此写法的讨论--add-opens=java.desktop/javax.swing=ALL-UNNAMED
但通常建议加上=号--add-opens java.desktop/javax.swing=ALL-UNNAMED
然后继续分析日志,看是否还有步骤1的警告日志 - 经过上述2个步骤一段时间的验证,将测试环境和产品环境JVM启动参数改为
--illegal-access=deny
。并继续分析日志,以确保代码中没有反射失败的情况。
将运行时JDK改为JDK17
我们的策略是:
- 先将测试环境RunTime JDK改为17并运行一段时间;如果测试环境运行稳定,则将产品环境RunTime JDK改为逐渐切换为17
- 但使用Jenkins build项目时,编译时JDK依旧保持JDK11
- 简而言之,把JDK11编译出来的包放在JDK17中跑
优化Build Java应用时的Jenkins Job
目前build java应用之后,tomcat使用的JDK是hard code在相关的jenkins job里的。
这种写法不利于当前以及后续JDK升级的测试。原因是:这些jenkins job的配置基本由devops维护,我们实际使用者如果有什么需求只需要给他们提就可以了,他们会给我们创建单独的job。
在了解了他们的实际工作过程之后,我发现完全没必要copy、创建新的jenkins job,只需要在原有的jenkins job上添加一个参数,例如:RUNTIME_JDK,build项目时将该参数传给shell脚本,在构建完docker镜像之后,启动Tomcat之前,执行一个switchJDK.sh
即可完成JDK的切换。
这样做的好处是
- 从长期来讲,devops的工作量反而是减轻了且有助于他们的代码维护,他们只需要定期把未来要使用JDK版本纳入到我们使用的自动以的docker镜像中,就无需介入JDK的升级工作。
- 对于我们实际升级JDK的人以及其他开发或QA同事来说,有了这个参数,前期JDK升级时,如果测试环境有什么问题,可以迅速切换回正在使用的JDK,不会影响当前工单的smoke test。
将编译时JDK改为JDK17
产品环境运行时JDK改为17之后,运行一段时间,如果产品环境稳定。则将编译时JDK改为JDK17。这一步主要是pom.xml
的改变
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
这个改变合并到主分支之后,build该项目的jenkins job需要同时做切换。指定编译时JDK为17
通知开发切换为JDK17
上述改动合并到主分支之后,JDK就完成了升级,需要通知开发下载和产品、测试环境一样的JDK 版本并切换,并修改IDEA中有关Java版本、编译的配置
写在最后
对所有Java应用,从JDK8到JDK11,所有的验证、测试、分批部署到产品、最终完成升级,我们用了接近半年时间完成升级。预计JDK17会比较顺利,可能4个月左右完成升级。
上面这些事情看起来不复杂,但整个过程要想保证对服务不造成影响,需要我们非常细心、谨慎,以及和其他团队充分配合才能一起完成这件事,这更考验我们除了编程之外的技能。
最后以一句话结尾:
只有那些有耐心做好简单事情的人,才能获得轻松完成困难事情的技能