从JDK 8到21:一次Spring Boot 2到3.2的完整迁移与避坑指南

📅 2026/6/30 14:20:43
从JDK 8到21:一次Spring Boot 2到3.2的完整迁移与避坑指南
1. 为什么需要从JDK 8升级到21如果你还在使用JDK 8和Spring Boot 2.x可能会觉得能用就行何必折腾。但作为一个经历过完整迁移的老司机我必须告诉你这次升级带来的收益远超你的想象。首先JDK 21是长期支持版本LTS这意味着未来几年你都能获得稳定的官方支持。其次性能提升非常显著 - 在我的实测中同样的服务在JDK 21上运行时GC停顿时间减少了40%吞吐量提升了25%。更重要的是Spring Boot 3.2带来了很多现代化特性。比如对虚拟线程的原生支持可以让你用同步代码写出异步性能的应用。还有GraalVM原生镜像支持能让你的应用启动时间从秒级降到毫秒级。这些都不是小打小闹的优化而是能真正改变你开发体验和生产效率的升级。2. 升级前的准备工作2.1 环境检查清单在开始升级前建议先做个完整的系统体检。我通常会创建一个这样的检查表确认当前项目的Maven/Gradle构建配置列出所有第三方依赖及其版本检查是否有使用废弃的API比如javax.servlet扫描代码中的反射调用点记录当前JVM参数和性能指标这个步骤看似繁琐但能帮你提前发现80%的潜在问题。我曾经在一个项目中省下了两周的调试时间就是因为提前发现了不兼容的MyBatis版本。2.2 搭建测试环境千万不要直接在生产环境上尝试升级建议按这个流程搭建测试环境# 使用Docker快速搭建测试环境 docker run -d --name test-mysql -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 mysql:8.0 docker run -d --name test-redis -p 6379:6379 redis:7.0然后在CI/CD流水线中加入新版本的测试任务。我习惯用GitHub Actions做多版本并行测试jobs: test: strategy: matrix: java: [8, 17, 21] steps: - uses: actions/setup-javav3 with: java-version: ${{ matrix.java }}3. 包名变更与反射限制3.1 javax.servlet到jakarta.servlet这是最明显的变更点。所有javax.servlet相关的import都需要改为jakarta.servlet。我推荐使用IntelliJ IDEA的全局替换功能按CtrlShiftR调出替换对话框勾选Regex选项输入javax\.servlet作为查找内容输入jakarta.servlet作为替换内容对于Maven项目记得更新相关依赖dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId version6.0.0/version scopeprovided/scope /dependency3.2 反射安全增强JDK 16开始加强了反射限制这会影响很多框架。比如Dubbo的客户端代理生成就会出问题。解决方法是在启动时添加JVM参数--add-opensjava.base/java.langALL-UNNAMED --add-opensjava.base/java.ioALL-UNNAMED --add-opensjava.base/java.utilALL-UNNAMED对于Maven编译需要确保保留参数名称plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-compiler-plugin/artifactId version3.11.0/version configuration parameterstrue/parameters /configuration /plugin4. 关键组件升级指南4.1 MyBatis升级从Spring Boot 2.x到3.2MyBatis需要同步升级。这里有个坑点MyBatis-Spring的版本必须匹配。我推荐这样配置dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version3.0.3/version /dependency如果使用MyBatis-Plus记得也要升级dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency4.2 Apache HttpClient升级HttpClient 4.x已经不再维护必须升级到5.xdependency groupIdorg.apache.httpcomponents.client5/groupId artifactIdhttpclient5/artifactId version5.2.1/version /dependencyAPI有一些变化比如// 旧版 CloseableHttpClient httpClient HttpClients.createDefault(); // 新版 CloseableHttpClient httpClient HttpClients.custom() .setConnectionManager(PoolingHttpClientConnectionManagerBuilder.create().build()) .build();5. 虚拟线程实战5.1 启用虚拟线程JDK 21的虚拟线程是个游戏规则改变者。在Spring Boot 3.2中启用非常简单Bean public TomcatProtocolHandlerCustomizer? protocolHandlerVirtualThreadExecutorCustomizer() { return protocolHandler - { protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); }; }对于Dubbo服务端虽然官方不建议使用虚拟线程但可以通过SPI实现public class VirtualThreadPool implements ThreadPool { Override public Executor getExecutor(URL url) { return Executors.newVirtualThreadPerTaskExecutor(); } }然后在resources/META-INF/dubbo/com.alibaba.dubbo.common.threadpool.ThreadPool文件中添加virtualcom.your.package.VirtualThreadPool5.2 虚拟线程的注意事项虚拟线程不是银弹有几个关键限制synchronized块内发生阻塞会pin住载体线程JNI调用期间不会释放载体线程线程局部变量(ThreadLocal)会有性能损耗建议添加这些JVM参数来监控问题-Djdk.tracePinnedThreadsfull -Djdk.virtualThreadScheduler.parallelism16. 测试与验证6.1 单元测试调整JUnit需要升级到5.x版本dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter-engine/artifactId version5.10.1/version scopetest/scope /dependency注意Test注解现在来自org.junit.jupiter.api包。6.2 集成测试策略建议采用渐进式测试策略先确保所有单元测试通过然后测试核心业务流最后进行全链路压测使用Testcontainers做集成测试非常方便Testcontainers class UserServiceIT { Container static MySQLContainer? mysql new MySQLContainer(mysql:8.0); Test void testCreateUser() { // 测试代码 } }7. 性能调优7.1 GC参数调整JDK 21的ZGC有了很大改进推荐配置-XX:UseZGC -XX:ZCollectionInterval5 -XX:ZAllocationSpikeTolerance5.0对于内存小于8GB的应用可以考虑Shenandoah-XX:UseShenandoahGC -XX:ShenandoahGCHeuristicscompact7.2 监控指标升级后要特别关注这些指标虚拟线程的创建和销毁速率载体线程的利用率GC停顿时间内存使用模式可以用PrometheusGrafana搭建监控management: endpoints: web: exposure: include: prometheus metrics: tags: application: ${spring.application.name}8. 回滚方案即使准备再充分也要有回滚计划。我建议保留旧版本的Docker镜像准备回滚数据库脚本记录当前性能基准制定分阶段回滚策略回滚时特别注意数据兼容性问题特别是如果新版本已经写入了新格式的数据。