01
背景
将公司的 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 是一个无法绕过的前提条件。
02
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。
03
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
迁移成本巨大?
- 全局代码修改:这不仅仅是简单的“查找和替换”。项目中所有涉及到 Servlet API、JPA、Bean Validation 等规范的 import 语句都需要修改。对于一个成熟的大型项目,这涉及成百上千个文件的改动。
- 整个依赖生态系统的颠覆:这是最棘手的问题。不仅仅是我们的代码,我们所依赖的所有第三方库(如数据库驱动、消息队列客户端、缓存工具、安全框架、自定义 Starters 等)都必须提供与 Jakarta EE 兼容的新版本。
- 传递性依赖冲突:即使我们升级了直接依赖,这些依赖的传递性依赖(它们依赖的库)可能仍然停留在 javax 命名空间,这将导致灾难性的类路径冲突(ClassNotFoundException, NoClassDefFoundError),解决这些冲突非常耗时且痛苦。
- 潜在的“深水区” Bug:某些库可能声称兼容 Jakarta EE,但在边缘场景下存在未被发现的 Bug。这种因底层 API 变更引入的问题通常难以定位和修复。
决策结论:
选择 Spring Boot 2.7.18 意味着我们可以完全避免这个高风险、高成本的迁移过程,将团队的宝贵时间和精力聚焦于业务功能的开发和交付上。
04
依赖库版本
依赖库版本可通过如下链接进行获取:
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
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
05
升级步骤
开发工具准备:
-
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、消息队列等)的连接和交互是否正常
06
总结
本次 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 的新特性(如虚拟线程),充分发挥新版本的技术优势,持续提升系统的性能和开发效率。

