资讯详情

资讯详情

Java 项目从 JDK 8 升级到 JDK 17 踩过的 12 个坑

去年把公司一个跑了五年的 Spring Boot 单体从 JDK 8 升到 JDK 17前后折腾了将近三周。网上搜了一圈升级指南要么太笼统改一下 pom 里的 source 和 target 就行了要么太老还在讲 JDK 9 的 --illegal-accesspermit这个参数在 17 里已经彻底删了。这篇把我实际踩过的 12 个坑按发现顺序写下来每个坑给出现象、原因和解法。适用场景Spring Boot 2.x/3.x Maven 常见国产中间件MyBatis/Dubbo/Fastjson/Druid。Gradle 项目思路一样命令换一下就行。前置准备升级前先把这些事做了能省后面大量排查时间确认 Spring Boot 版本JDK 17 最低要求 Spring Boot2.5.x官方支持矩阵推荐直接升到2.7.x过渡或者一步到位上3.x3.x 强制 JDK 17但会引入 javax → jakarta 的大改跑一遍完整测试用例留个基线升级后对比哪些是新引入的 failure把所有依赖列出来mvn dependency:tree deps.txt搜一下每个依赖的 JDK 17 兼容版本新建分支别在 main 上直接改升级过程中可能要反复回滚好了开始踩坑。坑 1编译直接报 invalid target release: 17现象[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile Fatal error compiling: error: invalid target release: 17原因maven-compiler-plugin 版本太老3.8.x 及以前不认 17 这个 target。另外也可能是 IDEA 或 CI 环境用的 JAVA_HOME 还指向 JDK 8。解法plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version !-- 最低 3.10.1 才支持 JDK 17 -- configuration source17/source target17/target release17/release !-- 推荐用 release 代替 sourcetarget -- /configuration /pluginrelease 标签是 JDK 9 引入的等效于 source target bootclasspath 三合一比分开写更安全。然后确认环境变量java -version # 应该显示 17.x mvn -version # 看 Java version 那一行 echo $JAVA_HOME # 指向 JDK 17 安装目录CI 上记得改构建镜像或 JDK 选择配置。坑 2javax.xml.bind 找不到JAXB 被移除现象java.lang.ClassNotFoundException: javax.xml.bind.JAXBException或者编译期就报 package javax.xml.bind does not exist。原因JDK 9 开始 javax.xml.bindJAXB、javax.annotation、javax.activation、javax.xml.wsJAX-WS、CORBA 这些 Java EE 模块被标记为 deprecatedJDK 11 正式删除。JDK 8 里它们是自带的升上去就没了。解法手动加依赖。!-- JAXB -- dependency groupIdjakarta.xml.bind/groupId artifactIdjakarta.xml.bind-api/artifactId version4.0.1/version /dependency dependency groupIdorg.glassfish.jaxb/groupId artifactIdjaxb-runtime/artifactId version4.0.4/version /dependency !-- javax.annotationPostConstruct、Resource 等 -- dependency groupIdjakarta.annotation/groupId artifactIdjakarta.annotation-api/artifactId version2.1.1/version /dependency注意如果你还在 Spring Boot 2.x用 javax.* 包名要用 jakarta.xml.bind-api 的3.x版本包名仍是 javax.xml.bind而不是 4.x4.x 改成了 jakarta.xml.bind。Spring Boot 3.x 才用 4.x 系列。这个坑最恶心的地方在于有些不是你直接用的是某个第三方 jar 内部依赖了 JAXB。比如一些老的 WebService 客户端、Excel 导出库POI 的某些版本、甚至某些 JSON 序列化框架。这时候报错堆栈里看到的是第三方包名需要顺藤摸瓜找到谁在用然后升级那个包。坑 3反射访问 JDK 内部类被拒InaccessibleObjectException现象java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not opens java.lang to unnamed module xxx原因这是 JDK 17 升级最大的坑。JDK 9 引入模块系统JPMSJDK 16 开始默认 --illegal-accessdenyJDK 17彻底删除了 --illegal-access 选项。任何通过反射访问 java.base 模块内部字段/方法的代码如果没有显式 opens一律报 InaccessibleObjectException。受影响的典型场景Fastjson 1.x序列化时反射获取 private 字段MyBatis某些 TypeHandler 用了反射Spring Framework 5.x内部大量反射不过 Spring 5.3.x 已经做了兼容处理Lombok编译期注解处理器需要访问 com.sun.tools.javac 内部 API各种 BeanUtils / 深拷贝工具反射 copy 字段CGLIB / ByteBuddy动态代理生成Dubbo序列化/反序列化Druid监控面板用了反射解法方案 A推荐升级依赖到兼容 JDK 17 的版本库JDK 17 兼容版本Fastjson2.0.x重写了反射逻辑MyBatis3.5.10Spring Boot2.7.x内含 Spring Framework 5.3.xLombok1.18.22Dubbo3.1.xDruid1.2.16CGLIB用 Spring 内置的 repackaged 版本即可方案 B临时过渡JVM 启动参数加 --add-opensjava --add-opens java.base/java.langALL-UNNAMED \ --add-opens java.base/java.lang.reflectALL-UNNAMED \ --add-opens java.base/java.utilALL-UNNAMED \ --add-opens java.base/java.mathALL-UNNAMED \ --add-opens java.base/sun.reflect.annotationALL-UNNAMED \ -jar your-app.jarMaven 项目里加到 maven-surefire-plugin跑测试用plugin artifactIdmaven-surefire-plugin/artifactId version3.1.2/version configuration argLine --add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.utilALL-UNNAMED /argLine /configuration /plugin方案 B 是临时续命用的不要长期依赖。每次碰到新的 InaccessibleObjectException 就加一行 --add-opens加到十几行的时候你就该认真升级依赖了。坑 4CMS 垃圾回收器没了现象Unrecognized VM option UseConcMarkSweepGC Error: Could not create the Java Virtual Machine.原因CMSConcurrent Mark Sweep在 JDK 9 标记为 deprecatedJDK 14 正式移除。如果你的 JVM 启动参数或 Dockerfile 里有 -XX:UseConcMarkSweepGC直接启动失败。解法换 G1JDK 9 之后默认就是 G1# 删掉这些 CMS 相关参数 # -XX:UseConcMarkSweepGC # -XX:CMSParallelRemarkEnabled # -XX:CMSInitiatingOccupancyFraction75 # -XX:UseCMSInitiatingOccupancyOnly # 换成 G1大多数场景不用显式指定JDK 17 默认就是 G1 -XX:UseG1GC -XX:MaxGCPauseMillis200 -XX:G1HeapRegionSize8m -XX:InitiatingHeapOccupancyPercent45如果你对延迟极度敏感比如交易系统 P99 要求 10ms可以考虑 ZGC-XX:UseZGC -XX:ZGenerationalZGC 在 JDK 17 还是实验性的production-ready 要 JDK 21但已经能用。坑 5一些老的 JVM 参数不认了除了 CMS还有一堆参数在 JDK 9~17 之间被移除或废弃参数状态替代-XX:UseConcMarkSweepGCJDK 14 移除G1默认或 ZGC-XX:PrintGCDetailsJDK 9 废弃-Xlog:gc* (统一日志)-XX:PrintGCTimeStampsJDK 9 废弃-Xlog:gc*::time-Xloggc:fileJDK 9 废弃-Xlog:gc*:filegc.log-XX:PermSize / -XX:MaxPermSizeJDK 8 就没了MetaspaceSize / MaxMetaspaceSize-XX:UseParNewGCJDK 10 移除G1 自带并行 Young GC-XX:AggressiveOptsJDK 11 废弃删掉现代 JIT 已经足够好解法把 JVM 参数过一遍用 JDK 17 的统一日志格式替换老的 GC 日志参数# JDK 8 写法 -XX:PrintGCDetails -XX:PrintGCDateStamps -Xloggc:/logs/gc.log # JDK 17 写法 -Xlog:gc*,gcagetrace,safepoint:file/logs/gc.log:time,uptime,level,tags:filecount5,filesize20m排查技巧如果不确定某个参数还能不能用跑一下java -XX:PrintFlagsFinal -version | grep 你的参数名或者加 -XX:UnlockDiagnosticVMOptions 看诊断信息。坑 6Lombok 编译失败现象java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in unnamed module 0x...) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module 0x...原因Lombok 依赖 jdk.compiler 模块的内部 APIcom.sun.tools.javac.*JDK 17 的强封装直接拒绝访问。解法升级 Lombok 到 1.18.22最好 1.18.30新版已经在内部做了 --add-opens 处理如果 Maven 编译还报错在 maven-compiler-plugin 里显式加 opensplugin artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration release17/release compilerArgs arg-J--add-opensjdk.compiler/com.sun.tools.javac.processingALL-UNNAMED/arg arg-J--add-opensjdk.compiler/com.sun.tools.javac.treeALL-UNNAMED/arg arg-J--add-opensjdk.compiler/com.sun.tools.javac.utilALL-UNNAMED/arg arg-J--add-opensjdk.compiler/com.sun.tools.javac.codeALL-UNNAMED/arg arg-J--add-opensjdk.compiler/com.sun.tools.javac.compALL-UNNAMED/arg /compilerArgs annotationProcessorPaths path groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.30/version /path /annotationProcessorPaths /configuration /pluginIDEA 里确认 Lombok 插件版本也是最新的Settings → Plugins → 搜 Lombok → Update坑 7Fastjson 序列化异常或字段丢失现象com.alibaba.fastjson.JSONException: write javaBean error序列化后某些字段值为 null明明有值反序列化报 autoType is not support原因Fastjson 1.x 大量使用反射获取 private 字段JDK 17 的模块系统阻止了这种访问。另外 Fastjson 1.x 的 autoType 安全策略在新 JDK 下行为也可能变化。解法推荐方案迁移到 Fastjson2dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version2.0.43/version /dependencyFastjson2 兼容大部分 1.x 的 API包名从 com.alibaba.fastjson 改成 com.alibaba.fastjson2也提供了兼容包让你渐进迁移!-- 兼容包保留 com.alibaba.fastjson 包名底层用 fastjson2 实现 -- dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2-extension/artifactId version2.0.43/version /dependency如果暂时升不了 Fastjson代码耦合太深临时解法是在启动参数加--add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.utilALL-UNNAMED --add-opens java.base/java.mathALL-UNNAMED --add-opens java.base/java.timeALL-UNNAMED但强烈建议尽快迁到 Fastjson2 或者干脆换 JacksonSpring Boot 默认集成的就是 Jackson少一个依赖少一份风险。坑 8Nashorn 脚本引擎没了现象javax.script.ScriptException: java.lang.ClassNotFoundException: jdk.nashorn.api.scripting.NashornScriptEngineFactory原因NashornJDK 内置的 JavaScript 引擎在 JDK 11 deprecatedJDK 15 正式移除。如果你的代码里有 ScriptEngine engine new ScriptEngineManager().getEngineByName(javascript)JDK 17 上返回 null。谁在用 Nashorn规则引擎Drools 的某些脚本规则、动态表达式计算、一些老的模板引擎、以及在 Java 里跑一段 JS 逻辑这种写法。解法方案 A引入独立的 Nashorn 发行版dependency groupIdorg.openjdk.nashorn/groupId artifactIdnashorn-core/artifactId version15.4/version /dependency代码不用改只是引擎从 JDK 内置变成了外部依赖。方案 B换 GraalVM JavaScriptdependency groupIdorg.graalvm.js/groupId artifactIdjs/artifactId version23.0.2/version /dependency dependency groupIdorg.graalvm.js/groupId artifactIdjs-scriptengine/artifactId version23.0.2/version /dependencyGraalJS 性能比 Nashorn 好很多也支持 ES2022。方案 C如果只用了简单的表达式计算考虑换 SpELSpring Expression Language或 MVEL / Aviator比维护一个完整 JS 引擎轻量得多。坑 9Spring Boot 版本必须升连带一堆 starter 不兼容现象java.lang.NoClassDefFoundError: org/springframework/core/NativeDetector或者各种 Bean 创建失败、auto-configuration 报错。原因Spring Boot 2.5 以下不支持 JDK 17。Spring Framework 5.3.x 是第一个官方支持 JDK 17 的版本。如果 Boot 版本不动只升 JDKSpring 内部的一些反射操作和字节码生成会出问题。解法升级路径推荐两步走Spring Boot 2.3/2.4/2.5 → 2.7.xJDK 17 可用→ 3.x可选JDK 17 强制Spring Boot 2.7.x 是过渡版本的安全港——它支持 JDK 17但还是用 javax.* 包名改动量小。3.x 则强制 jakarta.*需要全局替换包名用 OpenRewrite 或 IDEA 的全局替换。升 Spring Boot 时连带要升的组件Spring Boot 2.7 推荐版本MyBatis Spring Boot Starter2.3.xMyBatis-Plus3.5.3Druid Spring Boot Starter1.2.16PageHelper5.3.xSwagger / Knife4jKnife4j 3.0.3或迁到 SpringDoc OpenAPIRedisLettuce跟 Boot 版本走Spring Security跟 Boot 版本走改完 pom 之后跑 mvn dependency:tree | grep omitted for conflict 看看有没有版本冲突有的话用 dependencyManagement 统一。坑 10Docker 基础镜像要换现象CI/CD 构建失败或者容器跑起来报各种 Class not found。原因Dockerfile 里还在用 FROM openjdk:8-jdk-alpine但代码已经编译成 JDK 17 字节码了class file version 61运行时的 JDK 8class file version 52自然加载不了。解法# 旧 FROM openjdk:8-jdk-alpine # 新推荐 Eclipse Temurin体积小且有 alpine 版 FROM eclipse-temurin:17-jre-alpine # 如果需要完整 JDK比如要用 jmap/jstack 调试 FROM eclipse-temurin:17-jdk-alpine注意区分 JRE 和 JDK生产环境用 JRE 就够了比 JDK 小 200MB除非你需要 jcmd、jstack、jmap 这些诊断工具。如果用多阶段构建# 构建阶段 FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline -B COPY src ./src RUN mvn package -DskipTests -B # 运行阶段 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT [java, -Xms512m, -Xmx1024m, -jar, app.jar]坑 11单元测试大面积失败现象mvn test 几十个测试报错主要是Mockito cannot mock this class: class xxxInaccessibleObjectException又是反射PowerMock 相关的各种奇葩错误原因Mockito用 ByteBuddy 生成 mock 对象老版本不兼容 JDK 17 的强封装PowerMock更惨——它用自定义 ClassLoader 反射修改 final 字段在 JDK 17 下基本废了maven-surefire-plugin老版本不会自动传 --add-opens 给测试 JVM解法升级 Mockito 到 4.x推荐 5.xdependency groupIdorg.mockito/groupId artifactIdmockito-core/artifactId version5.8.0/version scopetest/scope /dependency dependency groupIdorg.mockito/groupId artifactIdmockito-junit-jupiter/artifactId version5.8.0/version scopetest/scope /dependency干掉 PowerMock这是最好的时机。PowerMock 维护基本停了JDK 17 兼容性极差。用 Mockito 4 的 mockStatic、mockConstruction 替代 PowerMock 的 static mock 功能// PowerMock 写法已废弃 RunWith(PowerMockRunner.class) PrepareForTest({StaticUtil.class}) public class OldTest { Test public void test() { PowerMockito.mockStatic(StaticUtil.class); when(StaticUtil.getValue()).thenReturn(mocked); } } // Mockito 4 写法 ExtendWith(MockitoExtension.class) public class NewTest { Test void test() { try (MockedStaticStaticUtil mocked mockStatic(StaticUtil.class)) { mocked.when(StaticUtil::getValue).thenReturn(mocked); // assertions... } } }surefire 加 opensplugin artifactIdmaven-surefire-plugin/artifactId version3.1.2/version configuration argLine --add-opens java.base/java.langALL-UNNAMED --add-opens java.base/java.lang.reflectALL-UNNAMED --add-opens java.base/java.utilALL-UNNAMED /argLine /configuration /plugin如果用JUnit 4趁这次机会迁到JUnit 5ExtendWith 替代 RunWithBeforeEach 替代 Before。Spring Boot 2.7 默认已经引入了 JUnit 5只需要把老测试的注解换一下。坑 12线上运行一段时间后 Metaspace OOM现象服务跑几天后报 java.lang.OutOfMemoryError: MetaspaceJDK 8 上从来没出现过。原因JDK 8 的 Metaspace 默认没有上限受系统内存限制而很多人升级到 JDK 17 后照搬了老的 JVM 参数里面可能有一个被忽略的 -XX:MaxMetaspaceSize256m。JDK 17 的 class 元数据比 JDK 8 占用略大模块信息、方法句柄等再加上某些动态代理/字节码生成库CGLIB、Groovy、Nashorn会持续生成新 class256m 可能真的不够了。解法# 适当放大 Metaspace -XX:MetaspaceSize256m -XX:MaxMetaspaceSize512m # 加上监控参数提前预警 -Xlog:classloadinfo:file/logs/classload.log:time,uptime:filecount3,filesize10m另外用 jcmd pid VM.metaspace 或 jcmd pid GC.class_stats 看 Metaspace 里到底装了什么。如果某个 ClassLoader 加载了几千个类大概率是动态代理泄漏——检查是不是每次请求都 new ProxyFactory() 而没有缓存。附带建议升完 JDK 后把 JVM 参数整体 review 一遍不要简单照搬 JDK 8 时代的配置。JDK 17 的 G1 比 JDK 8 的 G1 成熟太多很多手动调优参数比如 -XX:G1ReservePercent、-XX:MaxTenuringThreshold可以删掉让 JVM 自适应。升级后的好处给自己一个坚持的理由踩完这 12 个坑之后说下实际收益性能同一台机器同一个接口JDK 17 比 JDK 8 的 P99 延迟降了约 15~20%吞吐量提升 10%。这主要归功于 G1 的改进JDK 12 的 Deterministic G1、JDK 14 的 NUMA-aware 分配和 JIT 编译器优化。内存G1 在 JDK 17 下的内存回收效率明显好于 JDK 8同样的 -Xmx 配置GC 暂停时间从平均 200ms 降到 50~80ms。语言特性record、sealed class、text block、switch expression、instanceof pattern matching 这些用起来真回不去了。DTO 类从 30 行缩到 3 行代码可读性提升很大。安全JDK 8 的公开免费更新早就停了Oracle 商业支持到 2030但你得付费JDK 17 是 LTS各厂商Temurin、Corretto、Zulu都提供至少到 2029 年的免费安全更新。容器化JDK 17 对容器环境的感知CPU/内存限制识别比 JDK 8 好太多。JDK 8 早期版本在 Docker 里会读到宿主机的全部内存和 CPU导致 JVM 分配远超容器 limit 然后被 OOMKill。虽然 8u191 修了但 JDK 17 的 UseContainerSupport 更成熟。一份可以直接抄的升级 checklist最后把整个过程压缩成一个 checklist按顺序做□ 1. 新建分支 jdk17-upgrade □ 2. pom.xml 改 maven-compiler-plugin → 3.11 / release17 □ 3. pom.xml 改 Spring Boot → 2.7.x □ 4. 加 JAXB / javax.annotation 独立依赖 □ 5. 升 Lombok → 1.18.30 □ 6. 升 Fastjson → Fastjson2 或换 Jackson □ 7. 升 Mockito → 5.x删 PowerMock □ 8. 升 maven-surefire-plugin → 3.1.2配 --add-opens □ 9. 清理 JVM 参数删 CMS、换 GC 日志格式 □ 10. Dockerfile 换基础镜像 → eclipse-temurin:17-jre-alpine □ 11. mvn clean test 全量跑测试修 failure □ 12. 本地启动冒烟测试看有无 InaccessibleObjectException □ 13. 性能压测对比JMeter / wrk确认没有退化 □ 14. 合入主干灰度发布整个流程如果是中等规模项目50~100 个类、十几个依赖大概需要 2~5 天。大项目几百个类、复杂的内部中间件可能需要 2~3 周。不管哪种先升依赖再升 JDK这个顺序不要反——反过来你会同时面对一堆报错根本分不清是依赖问题还是 JDK 问题。如果你正在做这个升级卡在哪一步了可以评论区说看到会回。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →