前阵子我接了个“老旧系统JDK基线升级”的任务,一个维护了多年的Spring Boot 2.6服务要从JDK 8迁到JDK 17。刚把这个事儿在项目群里一说,马上就有两种声音冒出来:一种说“都升17了,干脆连Spring Boot 3一起升了呗,免得以后重复折腾”,另一种说“Spring Boot 2升JDK 17肯定处处是坑,要不先退回JDK 11凑合吧”。
两种方案看起来都有道理,但我心里清楚,对业务代码量大的老系统来说,升Spring Boot 3不是简简单单换个版本号。包里那一堆javax.*import要改成jakarta.*,Spring Security 5的配置写法在6里也变了不少,加上各种中间件客户端、嵌入式容器兼容性,一折腾少说也要一两周,还可能把线上稳定状态给打破。至于退回JDK 11,就更亏了,等于从一个即将过时的LTS搬到另一个也快过时的LTS,白折腾一遍。
所以我当时的决定是:不降JDK,也不盲目上Boot 3,就让Spring Boot 2这个老伙计在JDK 17上好好跑起来。这套改造方案前前后后花了一个星期,从版本梳理、编译链路调整到线上参数配置都试过一轮,目前服务已经在JDK 17上稳定运行了两个多月。这篇文章想把这些实操经验完整记录下来,给同样处境的人一个能直接抄作业的参考答案。
1. 为什么我坚持不降级:老项目吃JDK 17红利,完全不用动Spring Boot主版本
先说结论:Spring Boot 2.x和JDK 17从来就不是水火不容的关系。Spring Boot 2.5.6开始,官方就已经把Java 17纳入了支持范围,后续的2.6.x、2.7.x都对JDK 17做了完整的适配测试。所以,“Spring Boot 2必须配JDK 8/11”这个想法是刻板印象,JDK 17要求的是Spring Framework 5.3.x以上,而Spring Boot 2.6、2.7内部的Spring Framework版本早就达标了。
真正让人犹豫的是另一个问题:既然要动JDK,为什么不同时把Boot主版本也升级了?我的判断是这样:
- 升级Boot 3意味着全量替换包坐标,从
javax.servlet到jakarta.servlet,从javax.annotation到jakarta.annotation,涉及代码改动面非常大。老项目里如果还依赖Spring Security 5的WebSecurityConfigurerAdapter,升级Boot 3就等于要重写认证授权这一整块逻辑。风险直接爆表。 - 降回JDK 11不是“退一步海阔天空”,而是把问题延后一两年再遇到一次。JDK 11虽然也是LTS,但它的免费更新周期早就不如JDK 17宽裕,与其再折腾一次,不如一步到位。
- Spring Boot 2.7.x其实是2.x系列的最后一个大版本,官方把大量补丁和兼容性修复都集中在这个版本上,JDK 17相关的问题相对最少。2.x生命周期虽然已渐入尾声,但对于内部系统完全够用,这也给了我们留足时间去规划以后的Boot 3迁移,而不是被一个JDK升级逼着仓促搬家。
所以,我最后采用了这个看起来“保守”的路线:业务代码不怎么动,Spring Boot 2.7.x继续用,只把JDK从8切到17。对应地,需要重点解决三件事:版本基线校准、反射访问限制、隐藏依赖的兼容性问题。
2. JDK 17真正的路障不是语法,是模块强封装让“反射自由”失效了
很多项目从JDK 8切到JDK 17,第一反应是“我的代码又没怎么写Java 17的新语法,凭什么跑不起来”。这句话对了一半,大部分业务代码确实不用改,可问题出在那些跑在JVM里的第三方库身上,它们在JDK 8时代大量使用反射去访问JDK内部的东西,到了JDK 17这扇门被系统管理员锁死了。
2.1 JEP 396与强封装:从“给警告”到“直接拒绝”
JDK 9开始引入模块系统,JDK内部API被封装到各个模块里,比如java.lang、java.util这些包都属于java.base模块。从JDK 16开始,JEP 396把“强封装内部API”从默认关闭变成了默认开启,第三方库再想靠反射去访问java.base模块里非导出的包,就不再是警告,而是直接抛异常。
JDK 17正是默认使用这种严格封装的第一个LTS版本。用大白话讲,以前你住的小区大门敞开,谁都能进来逛逛,顶多被保安盯一下;现在物业装了门禁,没有门禁卡的只能站在门外干瞪眼。那些老库的反射代码,就是没有门禁卡的访客。
2.2 Spring Boot 2项目里最常见的“全新报错”形态
在JDK 8时代,你大概率从来没见过这些异常,但从JDK 17开始,一旦触发就会立刻出现,而且异常信息往往写得很清楚:
- 反射访问被拒,日志长这样:
java.lang.reflect.InaccessibleObjectException: Unable to make field private static final java.lang.Class[] java.util.Collections$EmptyList.EMPTY_LIST accessible: module java.base does not "opens java.util" to unnamed module- CGLIB代理生成时也可能报错:
java.lang.IllegalAccessError: class org.springframework.cglib.proxy.... cannot access class ...- 更隐晦的还有这种:老字节码库不会读新版class文件,直接提示
Unsupported class file major version 61。
这些报错本质上都指向同一件事:某个JAR包或框架代码还在用JDK 8时代的方式做反射、字节码增强、CGLIB代理或Unsafe操作,而JDK 17把这条路堵死了。
2.3 Spring Boot 2应用里最容易撞墙的代码层
根据我的经验,JDK 17反射问题不是均匀分布在所有代码里的,主要集中在以下几层:
- Spring AOP/CGLIB代理:使用
@Configuration、@ConfigurationProperties、@EnableAsync等注解时,框架可能生成CGLIB子类来增强对象。Spring Framework 5.3.x已经为此做了兼容,但如果Boot版本太旧或者引入了旧版CGLIB,一样可能撞墙。 - ORM和JSON库:MyBatis、Fastjson、Gson这类库普遍使用反射读写字段,对JDK内部类的访问需求比业务代码多得多。
- 连接池和网络库:HikariCP、Netty、Jedis等在某些初始化路径上会通过反射访问
java.base模块里的类,如果版本没有跟上