上周把公司的老项目从 JDK8 升到 JDK17,本以为改改配置就行,结果连续加班三天才搞定。那些藏在版本升级里的小陷阱,没踩过的人真的想不到。今天把遇到的问题和解决办法整理出来,说不定能帮你少加几天班。
一、编译时突然爆红?内部 API 被锁了
刚把项目 JDK 版本改成 17,编译界面红得像过年贴的春联。仔细一看,全是sun.misc.BASE64Encoder这种类找不到的错误。问了公司的老程序员才知道,JDK17 把这些内部 API 彻底封起来了,不像以前那样能随便用。
记得有个生成用户 token 的工具类,一直用sun.misc.Unsafe来做高效字符串拼接,现在直接报错。试了三种解决办法:
最省事的是加启动参数--add-exports java.base/sun.misc=ALL-UNNAMED,虽然能临时解决,但控制台警告刷个不停,像个随时会炸的炸弹,不敢用在生产环境。
稳妥点还是换标准 API:sun.misc.BASE64换成java.util.Base64,Unsafe的字符串操作改用StringBuilder。改完后测试了下性能,其实差不了多少,代码还更安全。
后来发现 JDK 自带的jdeps命令特别好用,输入jdeps --jdk-internals 你的主类名,能把所有依赖内部 API 的地方都列出来。建议升级前先跑一遍,心里有个数。
二、老依赖拖后腿,Excel 库成了绊脚石
项目里用了个 2018 年的 Excel 处理库,升级后一运行就报NoClassDefFoundError。查了文档才明白,这个库偷偷用了sun.misc包里的类,现在作者早就不维护了,只能自己想办法。
最后换了 Alibaba 的 EasyExcel,功能比老库还全,就是得改几处调用代码。改的时候发现,不光是 Excel 库,连 Spring Boot 都得升级到 2.6 以上版本才行,好在升级后很多莫名其妙的错误自动消失了。
有个同事遇到更棘手的情况,他用的某个报表工具找不到替代库,只能加--add-opens java.base/java.lang=ALL-UNNAMED参数开后门。不过他说这是权宜之计,打算年后自己重写这块功能。
三、ZGC 没想象中完美,内存多占 30%
听别人说 ZGC 垃圾回收器多厉害,启动时就加了-XX:+UseZGC参数,结果运维拿着监控图来找我:"你这升级怎么越升越费内存?"
原来 ZGC 为了保证低延迟,会预留更多内存当缓冲。后来调了两个参数好多了:
-XX:ZAllocationSpikeTolerance=5.0 # 允许短期内存波动
-XX:ZCollectionInterval=60 # 回收间隔设成60秒
改完内存占用只比原来高 5%,但系统响应速度明显变快,用户投诉都少了。不过要提醒一句,堆内存小于 4G 的项目就别折腾了,用默认的 G1 收集器足够。
四、反射代码全失效,SecurityManager 没了
登录模块里有段反射获取用户 ID 的代码,在 JDK8 里好好的,到 17 直接抛IllegalAccessException。查了才知道,JDK17 把 SecurityManager 删了,对权限控制更严了。
以前直接field.setAccessible(true)就行,现在得先问一句有没有权限:
Field field = obj.getClass().getDeclaredField("id");
if (field.canAccess(obj)) {
// 有权限直接用
} else {
try {
field.setAccessible(true);
} catch (SecurityException e) {
// 没权限就处理异常
}
}
项目里 11 处反射代码都得这么改,改到最后发现,其实很多地方用构造函数注入更简单,还不用担风险。
五、Lambda 当缓存 key,突然报序列化错误
用 Lambda 表达式当缓存 key 的地方,升级后报NotSerializableException。原来 JDK17 对 Lambda 的序列化加了限制,匿名实现类不能随便序列化了。
解决办法有两个:要么手动加序列化接口(Serializable & Runnable) () -> { ... },要么改用方法引用。我选了后者,代码看着还更清爽。
后来跟其他公司的程序员交流,发现大家都遇到过类似问题,看来这是个共性坑点。
六、启动脚本别照搬,有些参数早删了
原来的启动脚本里有-XX:+UseConcMarkSweepGC,在 JDK17 里直接启动失败。才想起 CMS 收集器在 JDK14 就被废弃了,17 里彻底删了。
还有-XX:+PrintGCDetails也不能用了,得换成Xlog:gc*。建议启动前先用java -XX:+PrintFlagsFinal查下参数还能不能用,别想当然地复制老脚本。
七、日期显示不对,默认时区变了
用户投诉订单时间不对,查日志发现SimpleDateFormat在 JDK17 里默认时区是 UTC,而 JDK8 默认是系统时区。这一下所有没指定时区的日期处理都出了问题。
趁机把所有日期代码都换成新 API 了:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
.withZone(ZoneId.of("Asia/Shanghai"));
虽然花了一下午,但彻底解决了线程安全问题,也算值了。
八、IDE 配置搞反了,编译运行版本不一致
最冤的是这个坑:pom.xml 里明明指定了 JDK17,调试时却总报错。最后发现 IDE 的 Module SDK 还是 JDK8,而运行配置用的是 17,难怪会乱套。
正确的做法是:先在 IDE 全局设置里加好 JDK17,再把项目的 SDK 和语言级别都设成 17,最后在 Maven 插件里加上<release>17</release>。
最后想说的话
升级 JDK17 前,建议先做三件事:用mvn dependency:tree查依赖兼容性,用jdeps扫内部 API,先在非核心模块试点。虽然过程有点折腾,但升级后系统的稳定性和性能提升真的明显。
现在项目跑了一个月,那些加班改 bug 的日子,都成了茶余饭后的谈资。你们升级时遇到过什么奇葩问题?来评论区分享下解决方案吧~