一、背景
将公司的 Java 技术栈从 JDK 11 升级到 JDK 21,不仅仅是一次常规的版本更新,更是一次对生产力、系统性能、安全性和未来技术竞争力的战略投资。JDK 11 作为上一个长期支持版(LTS),稳定可靠,但自其发布以来,Java 平台经历了十个版本的迭代,积累了大量革命性的新特性和底层优化。JDK 21 作为最新的 LTS 版本,是这些创新的集大成者。
升级的必要性
支持终结 :Oracle 对 JDK 11 的免费公开更新(Public Updates)已于 2023 年 9 月结束。这意味着,如果不购买商业支持,你的生产环境将 无法获得最新的安全补丁和错误修复 ,这对于任何暴露在网络环境下的应用来说都是一个严重的安全隐患。
框架和库的硬性要求 :主流的开源框架和库正在快速拥抱新的 JDK 版本。例如, Spring Framework 6 和 Spring Boot 3.x 已经将最低版本要求提升至 JDK 17。这意味着,如果你的团队想使用这些框架的最新功能、性能优化和安全修复,升级 JDK 是一个无法绕过的前提条件。
二、JDK21版本特性
虚拟线程
虚拟线程 (Virtual Threads - Project Loom) :这是 JDK 21 的 王牌特性**。它从根本上改变了 Java 的并发编程模型。
之前 (JDK 11) :我们依赖昂贵的平台线程(与操作系统线程 1:1 对应),通过复杂的异步编程(如 CompletableFuture)和线程池来处理高并发,代码复杂且难以调试。
现在 (JDK 21) :我们可以用极其轻量级的虚拟线程,以 同步、顺序的编码风格写出高并发程序 。一个 JVM 可以轻松创建数百万个虚拟线程,使 I/O 密集型应用的吞吐量得到 数量级的提升 ,同时 显著降低了代码的复杂性 。
新一代垃圾收集器 (GC)
ZGC 和 Shenandoah 的成熟 :这两款低延迟 GC 在 JDK 21 中已成为生产可用级别,能够将 GC 暂停时间控制在 亚毫秒级别 ,对于需要稳定低延迟的实时应用(如交易系统、实时推荐)至关重要。
G1 GC 的持续改进 :作为默认 GC,G1 在新版本中也获得了大量优化,吞吐量和延迟表现比 JDK 11 中更好。
新版本引入了大量语法糖和新特性,旨在消除冗长的“样板代码”,让代码更简洁、更安全、更具表达力。
语法糖和新特性
新版本引入了大量语法糖和新特性,旨在消除冗长的“样板代码”,让代码更简洁、更安全、更具表达力。
- Records (记录类 - JDK 16) :一句话定义不可变的数据载体类 (DTO/POJO),自动生成构造函数、equals()、hashCode()、toString() 和 getter。
JDK 11
public final class Point {
private final int x;
private final int y;
// + 构造函数, getters, equals, hashCode, toString... (约50行代码)
}
JDK 21
public record Point(int x, int y) { } // 1行代码搞定
- Switch 模式匹配 (Pattern Matching for Switch - JDK 21) :让 switch 语句变得前所未有的强大和安全,可以直接对对象的类型和属性进行判断,消除了繁琐的 if-else 和类型强转。
JDK 11
Object obj = ...;
if (obj instanceof String) {
String s = (String) obj;
System.out.println("String: " + s.toUpperCase());
} else if (obj instanceof Integer) {
// ...
}
JDK 21
Object obj = ...;
switch (obj) {
case String s -> System.out.println("String: " + s.toUpperCase());
case Integer i -> System.out.println("Integer: " + i);
default -> { }
}
- 文本块 (Text Blocks - JDK 15) :优雅地编写多行字符串,告别丑陋的 + 拼接和 \n 转义,尤其适合编写 SQL、JSON、HTML 等。
JDK 11:
String json = "{\n" + " \"name\": \"John\",\n" + " \"age\": 30\n" + "}";
JDK 21:
String json = """
{
"name": "John",
"age": 30
}
""";
其他重要特性
Record 模式 (Record Patterns, JDK 21):优雅地解构 Record 对象。
Sealed Classes (密封类, JDK 17):更精确地控制类的继承关系,构建更严谨的领域模型。
var 关键字的改进:让局部变量类型推断更强大。
更友好的 NullPointerExceptions:NPE 异常信息会明确指出哪个变量是 null。
三、Spring Boot版本选择
SpringBoot和JDK版本兼容
虽然Spring Boot 3.x 版本带来了许多激动人心的新特性,但其颠覆性的 javax.到 jakarta.命名空间迁移为项目带来了不可忽视的巨大挑战。从 Spring Boot 2.x 升级到 3.x 不仅仅是一次常规的版本升级,而是一次 伤筋动骨的底层 API 迁移。
什么是命名空间变更?
由于 Java EE 规范的所有权从 Oracle 转移到了 Eclipse 基金会,其名称也变更为 Jakarta EE。这导致了所有相关 API 的 Java 包名从 javax. 强制变更为 jakarta. 。例如:
javax.servlet.http.HttpServletRequest -> jakarta.servlet.http.HttpServletRequest
javax.persistence.Entity -> jakarta.persistence.Entity
javax.validation.constraints.NotNull -> jakarta.validation.constraints.NotNull
迁移成本巨大?
1、全局代码修改:这不仅仅是简单的“查找和替换”。项目中所有涉及到 Servlet API、JPA、Bean Validation 等规范的 import 语句都需要修改。对于一个成熟的大型项目,这涉及成百上千个文件的改动。
2、整个依赖生态系统的颠覆:这是最棘手的问题。不仅仅是我们的代码,我们所依赖的所有第三方库(如数据库驱动、消息队列客户端、缓存工具、安全框架、自定义 Starters 等)都必须提供与 Jakarta EE 兼容的新版本。
3、传递性依赖冲突:即使我们升级了直接依赖,这些依赖的传递性依赖(它们依赖的库)可能仍然停留在 javax 命名空间,这将导致灾难性的类路径冲突(ClassNotFoundException,NoClassDefFoundError),解决这些冲突非常耗时且痛苦。
4、潜在的“深水区” Bug:某些库可能声称兼容 Jakarta EE,但在边缘场景下存在未被发现的 Bug。这种因底层 API 变更引入的问题通常难以定位和修复。
决策结论:
选择 Spring Boot 2.7.18 意味着我们可以 完全避免 这个高风险、高成本的迁移过程,将团队的宝贵时间和精力聚焦于业务功能的开发和交付上。
四、依赖库版本
依赖库版本可通过如下链接进行获取:
https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#appendix.dependency-versions
https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent/2.7.18
| 依赖 | 版本 |
|---|---|
| org.springframework | 5.3.31 |
| spring-data-redis | 2.7.18 |
| spring-data-mongodb | 3.4.18 |
| commons-lang3 | 3.12.0 |
| commons-collections | 3.2.2 |
| commons-collections4 | 4.4 |
| jstl | 1.2 |
| guava | 32.1.3-jre |
| jackson-mapper-asl | 1.9.8 |
| jackson-core | 2.16.1 |
| hibernate-validator | 6.2.5.Final |
| javax.el | 3.0.0 |
| javax.validation | 2.0.1.Final |
| javax.xml.bind | 2.3.1 |
| suishen.com.baidu.disconf | 2.6.38-SNAPSHOT |
| org.reflections | 0.9.11 |
| lombok | 1.18.30 |
| org.jetbrains | 24.0.1 |
| suishen-libs | 3.0.0-jdk21-SNAPSHOT |
| suishen-redis | 3.0.0-jdk21-SNAPSHOT |
| suishen-webx-parent | 3.0-jdk21-SNAPSHOT |
| suishen-webx-core | 3.0-jdk21-SNAPSHOT |
| suishen-root-pom | 3.0-jdk21-SNAPSHOT |
五、升级步骤
开发工具准备:
- idea升级到2025.2版本
- maven使用3.6.1版本
- tomcat 8或9
步骤1、pom文件修改
(1)升级suishen-webx-parent版本
<parent>
<groupId>suishen-webx</groupId>
<artifactId>suishen-webx-parent</artifactId>
<version>3.0-jdk21-SNAPSHOT</version>
</parent>
(2)完成后确认项目中的其他依赖库版本:
步骤2、更改applicationContext-mongodb.xml配置
(1)原配置:spring-data-mongodb 1.10.15.RELEASE
!-- 定义mongo对象,对应的是mongodb官方jar包中的Mongo,replica-set设置集群副本的ip地址和端口,多个以英文逗号分割 -->
<mongo:mongo-client id="mongoClient" replica-set="${mongo.hostport}" credentials="${mongo.username}:${mongo.password}@${mongo.dbname}">
<mongo:client-options
connections-per-host="${mongo.connectionsPerHost}"
threads-allowed-to-block-for-connection-multiplier="${mongo.threadsAllowedToBlockForConnectionMultiplier}"
connect-timeout="${mongo.connectTimeout}"
max-wait-time="${mongo.maxWaitTime}"
socket-keep-alive="${mongo.socketKeepAlive}"
socket-timeout="${mongo.socketTimeout}"/>
</mongo:mongo-client>
<mongo:db-factory dbname="${mongo.dbname}" mongo-ref="mongoClient"/>
(2)更改后配置 :spring-data-mongodb 3.4.18
<!-- 构建连接字符串,使用 SpEL 表达式对用户名和密码进行 URL 编码,避免特殊字符问题 -->
<bean id="mongoConnectionString" class="com.mongodb.ConnectionString">
<constructor-arg value="#{'mongodb://' + T(java.net.URLEncoder).encode('${mongo.username}', 'UTF-8') + ':' + T(java.net.URLEncoder).encode('${mongo.password}', 'UTF-8') + '@${mongo.hostport}/${mongo.dbname}?authSource=${mongo.dbname}&maxPoolSize=${mongo.connectionsPerHost:500}&connectTimeoutMS=${mongo.connectTimeout:5000}&socketTimeoutMS=${mongo.socketTimeout:10000}&serverSelectionTimeoutMS=${mongo.maxWaitTime:5000}'}"/>
</bean>
<!-- 创建 MongoClient -->
<bean id="mongoClient" class="com.mongodb.client.MongoClients" factory-method="create">
<constructor-arg ref="mongoConnectionString"/>
</bean>
<!-- 创建 MongoDatabaseFactory -->
<bean id="mongoDbFactory" class="org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory">
<constructor-arg ref="mongoClient"/>
<constructor-arg value="${mongo.dbname}"/>
</bean>
步骤3、修改相关代码
mongo排序
修改前:new Sort(Sort.Direction.DESC, "startTime")
修改后:Sort.by(Sort.Direction.DESC, "startTime")
reids指令
修改前:ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.SUM).weightsByDouble(weightsDouble)
修改后:ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.SUM).weights(weightsDouble)
步骤4、eone流水线配置更改
基础环境:tomcat9_jdk21
步骤5、发布观察日志
- 应用启动日志:检查是否有类加载错误、依赖冲突或配置问题
- 性能指标:监控 CPU、内存使用情况,观察 GC 日志,确认 ZGC/G1 运行正常
- 功能验证:全面回归测试核心业务功能,确保升级后功能正常
- 错误日志:密切关注应用错误日志,特别关注与 JDK 版本相关的异常
- 第三方服务调用:验证与外部服务(如数据库、Redis、消息队列等)的连接和交互是否正常
六、总结
本次 JDK 21 升级是一次重要的技术迭代,不仅解决了 JDK 11 安全支持终止的问题,更为团队带来了:
- 长期技术支持:JDK 21 作为最新的 LTS 版本,将获得至少 8 年的安全更新支持
- 性能提升:虚拟线程、新一代 GC 等特性将显著提升应用的并发处理能力和响应速度
- 代码质量:Records、模式匹配等现代语法特性让代码更简洁、更安全、更易维护
- 技术竞争力:为未来采用 Spring Boot 3.x 等新技术栈奠定基础
升级过程中虽然需要处理依赖版本调整和部分代码适配,但通过选择 Spring Boot 2.7.18,避免了 Jakarta EE 命名空间迁移的巨大成本,在获得 JDK 21 核心优势的同时,保持了升级路径的平稳可控。
建议在升级完成后,逐步探索和应用 JDK 21 的新特性(如虚拟线程),充分发挥新版本的技术优势,持续提升系统的性能和开发效率。
作者介绍:
- 孙景亮 资深服务端开发工程师