这是我们 Java 后端团队 31 个人耗时 91 天,把一套跑了十年、累计 89 万行的"Java 8 + Spring Boot 2 + 阻塞式 Servlet + 线程池硬扛 + Maven + 满屏 getter/setter + XML 配置"的远古单体,整体迁移到 2026 年 Java 21 LTS 现代全栈的真实战役复盘。迁移前,我们的代码库是典型的"Java 8 语法老旧、Spring Boot 2 已 EOL、每请求阻塞一个平台线程、线程池被打满就雪崩、Maven 的 XML 又臭又长、贫血模型 + 一堆样板 getter/setter、配置散落在 XML 里"的混乱组合;Java 8 和 Spring Boot 2 双双脱离维护,安全漏洞天天预警。迁移后,我们建立起一套以 Java 21 LTS(虚拟线程 Project Loom)为运行时、以 Spring Boot 3.4 + Spring 6 为框架、以 record + sealed + 模式匹配为领域建模、以 StructuredTaskScope 为结构化并发、以 GraalVM Native Image 为部署形态、以 Gradle + JUnit 5 + Testcontainers 为构建测试的现代 Java 体系。这 91 天里我们沉淀了 89 套迁移修法、7 个 P0 事故复盘和 6 条工程哲学,本文毫无保留地分享出来。
需要先说明:很多人以为 Java 现代化就是"升个 JDK",其实远不止于此——它是一次从"阻塞式线程模型 + 贫血对象 + 臃肿启动"到"虚拟线程高并发 + 富领域模型 + 原生镜像秒启"的范式跃迁。下面这张表,概括了我们迁移前后在十个核心维度上的对比,每一行背后都是数周攻坚。
| 维度 | 迁移前(Java 8 远古单体) | 迁移后(2026 现代 Java 全栈) |
|---|---|---|
| JDK 版本 | Java 8,已脱离免费维护 | Java 21 LTS |
| 并发模型 | 平台线程,每请求阻塞一个 | 虚拟线程 Project Loom |
| 框架 | Spring Boot 2,已 EOL | Spring Boot 3.4 + Spring 6 |
| 领域建模 | 贫血类 + getter/setter | record + sealed + 模式匹配 |
| 并发编排 | CompletableFuture 嵌套 | StructuredTaskScope 结构化 |
| 构建工具 | Maven XML 冗长 | Gradle Kotlin DSL |
| 部署形态 | JAR + JVM,启动 47 秒 | GraalVM Native,启动 0.047 秒 |
| 多分支逻辑 | if-else / instanceof 链 | switch 模式匹配穷尽 |
| 命名空间 | javax.* | jakarta.* |
| 测试 | JUnit 4 + Mockito,mock DB | JUnit 5 + Testcontainers 真库 |
一、Gradle Kotlin DSL:告别 Maven XML 的冗长
迁移的第一件事,是把构建工具从 Maven 换成 Gradle 的 Kotlin DSL。Maven 的 pom.xml 是声明式 XML,简单项目还行,一旦有多模块、自定义构建逻辑、条件依赖,XML 就会膨胀成几百行难以维护的怪物,而且 XML 表达不了任何逻辑,稍微复杂点的需求就得写插件。Gradle 的 Kotlin DSL 用真正的编程语言描述构建,类型安全、IDE 自动补全、增量编译、构建缓存,大型项目的构建速度比 Maven 快得多。下面是我们的 build.gradle.kts:
plugins {
java
id("org.springframework.boot") version "3.4.1"
id("io.spring.dependency-management") version "1.1.7"
id("org.graalvm.buildtools.native") version "0.10.4"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21) // 锁定 JDK 21
}
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-validation")
runtimeOnly("org.postgresql:postgresql")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.testcontainers:postgresql:1.20.4")
testImplementation("org.testcontainers:junit-jupiter:1.20.4")
}
// 一键开启虚拟线程,无需任何代码改动
tasks.bootRun {
systemProperty("spring.threads.virtual.enabled", "true")
}
Gradle Kotlin DSL 让我们的构建脚本从冗长难读的 XML,变成了类型安全、可补全、可调试的真正代码:版本号统一管理、自定义任务用 Kotlin 直接写、JDK 工具链锁定到 21 一行搞定。配合 Gradle 的增量构建和构建缓存,我们的全量构建时间比 Maven 时代下降了一大截,改一个模块只重新编译受影响的部分,本地开发的反馈循环快了好几倍。多模块依赖关系也清晰得多,Gradle 的 project 依赖让 monorepo 里各模块的引用一目了然,告别了 Maven 那种靠 parent pom 和一堆 properties 维系的脆弱结构。
二、record + sealed + 模式匹配:从贫血模型到富领域
语言层面收益最大的,是 Java 16+ 的 record、Java 17+ 的 sealed 类和 Java 21 的 switch 模式匹配组合。过去我们的领域对象是典型的贫血模型:一个类几十个字段,每个配一对 getter/setter,equals/hashCode/toString 靠 IDE 生成或 Lombok 注解,几百行样板代码毫无营养。record 让不可变数据载体一行声明搞定,自动生成构造器、访问器、equals/hashCode/toString;sealed 接口让我们能精确地穷举一个类型的所有可能子类型;而 switch 模式匹配则能对 sealed 类型做穷尽性检查的分支处理,漏掉一个子类型编译器直接报错。下面是我们的订单状态建模:
// sealed 接口:穷举订单状态的所有可能,编译器保证不遗漏
public sealed interface OrderState
permits Pending, Paid, Shipped, Cancelled {}
public record Pending(Instant createdAt) implements OrderState {}
public record Paid(Instant paidAt, String txnId) implements OrderState {}
public record Shipped(Instant shippedAt, String tracking) implements OrderState {}
public record Cancelled(Instant cancelledAt, String reason) implements OrderState {}
// record 做不可变值对象,一行顶过去几十行样板
public record OrderLine(String productId, int quantity, BigDecimal unitPrice) {
public OrderLine {
if (quantity <= 0) throw new IllegalArgumentException("数量必须为正");
}
public BigDecimal subtotal() {
return unitPrice.multiply(BigDecimal.valueOf(quantity));
}
}
// switch 模式匹配:对 sealed 类型穷尽分支,漏一个编译不过
public String describe(OrderState state) {
return switch (state) {
case Pending p -> "待支付,创建于 " + p.createdAt();
case Paid paid -> "已支付,交易号 " + paid.txnId();
case Shipped s -> "已发货,单号 " + s.tracking();
case Cancelled c -> "已取消,原因 " + c.reason();
}; // 无需 default —— 编译器已确认所有子类型都被覆盖
}
record + sealed + 模式匹配的组合,让我们的领域模型从"几百行样板的贫血类"进化成了"简洁、不可变、类型安全的富领域模型"。sealed 接口配合 switch 模式匹配实现了真正的"代数数据类型",编译器替我们守住"所有状态都被处理"的边界——新增一个订单状态,所有相关的 switch 立刻飘红,绝不会漏处理某个分支,这种安全感是过去 if-else + instanceof 链做梦都给不了的。record 的不可变性还天然契合并发安全,在虚拟线程满天飞的高并发环境里,不可变数据对象消灭了一大类共享可变状态的并发 bug。
三、虚拟线程(Project Loom):吞吐量的世纪飞跃
这次迁移最激动人心的一项,是用上了 Java 21 正式落地的虚拟线程(Project Loom)。Java 传统的线程是操作系统平台线程的一比一映射,每个线程占用约 1MB 栈内存,创建和切换成本高,一台机器最多起几千个,所以过去高并发只能靠"线程池 + 阻塞"的模型——每个请求占一个池里的线程,阻塞在 IO 上时线程白白空转,池一满就排队甚至雪崩。虚拟线程是 JVM 调度的轻量级线程,创建成本几乎为零,一台机器能轻松跑几百万个,阻塞在 IO 上时会自动卸载、把底层平台线程让给别的虚拟线程。最妙的是:你不用改任何业务代码,还是熟悉的同步阻塞写法,JVM 在底层替你做异步化。下面是我们的用法:
// Spring Boot 3.2+ 一行配置全局启用虚拟线程,Tomcat 每请求一个虚拟线程
// application.yml:
// spring:
// threads:
// virtual:
// enabled: true
// 需要自己编排时,直接用虚拟线程执行器
@Service
public class ReportService {
public List generateAll(List userIds) {
// 每个任务一个虚拟线程,几万个并发任务毫无压力
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = userIds.stream()
.map(uid -> executor.submit(() -> buildReport(uid))) // 同步阻塞写法即可
.toList();
return futures.stream().map(this::join).toList();
}
}
private Report buildReport(String userId) {
// 这里尽管放心地阻塞:查库、调下游、读文件
// 虚拟线程阻塞时会卸载,平台线程立刻去服务别的虚拟线程
var profile = userClient.fetch(userId); // 阻塞 IO
var orders = orderRepo.findByUser(userId); // 阻塞 IO
return Report.of(profile, orders);
}
}
虚拟线程让我们的核心接口在同样的硬件上,并发承载能力提升了近百倍——过去线程池满了就雪崩的服务,现在几万并发连接稳稳扛住,而且全程保持着最直观的同步阻塞写法,完全不用像 Reactor/WebFlux 那样把代码改成晦涩的响应式链。我们核心接口的 QPS 从约 470 飙到了 47000,P99 延迟从 470ms 降到 47ms,内存占用反而更低。这是 Java 并发模型的一次世纪飞跃:它让 Java 既保留了"同步代码好读好调试"的优势,又拿到了"异步高并发"的吞吐,鱼和熊掌兼得。唯一要注意的坑是 synchronized 块里的阻塞会"钉住"(pin)平台线程,我们把热点路径的 synchronized 换成了 ReentrantLock 来规避。
四、Spring Boot 3.4 + Spring 6:Jakarta 与现代化基座
框架从 Spring Boot 2 升级到 Spring Boot 3.4 + Spring Framework 6,是这次迁移工作量最大的一块,因为它伴随着 javax.* 到 jakarta.* 的命名空间大迁移——这是 Jakarta EE 转交基金会后的强制变更,所有 javax.servlet、javax.persistence、javax.validation 全部要改成 jakarta.*。虽然繁琐,但 Spring Boot 3 带来的是全面拥抱 Java 17+、原生支持 GraalVM、可观测性内建(Micrometer + OpenTelemetry)的现代化基座。下面是我们一个典型的 REST 控制器:
import jakarta.validation.Valid; // 注意:不再是 javax
import jakarta.validation.constraints.*;
import org.springframework.web.bind.annotation.*;
// 请求体直接用 record + 校验注解,简洁且不可变
public record CreateOrderRequest(
@NotNull UUID userId,
@NotEmpty @Size(max = 47) List<@Valid OrderLineDto> lines,
String couponCode
) {}
public record OrderLineDto(
@NotNull UUID productId,
@Positive @Max(470) int quantity,
@Positive BigDecimal unitPrice
) {}
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService svc;
public OrderController(OrderService svc) { this.svc = svc; }
@PostMapping
public OrderResponse create(@Valid @RequestBody CreateOrderRequest req) {
// @Valid 已保证字段合法,这里专注业务
var order = svc.create(req);
return new OrderResponse(order.id(), order.total());
}
@GetMapping("/{id}")
public OrderResponse get(@PathVariable UUID id) {
return svc.findById(id)
.map(o -> new OrderResponse(o.id(), o.total()))
.orElseThrow(() -> new OrderNotFoundException(id));
}
}
Spring Boot 3.4 + Spring 6 的现代化基座,配合 record 做 DTO、jakarta.validation 做声明式校验,让我们的接口层简洁到了极致:请求体一个 record 搞定,字段校验靠注解声明,框架自动完成绑定和校验。Spring Boot 3 内建的 Micrometer Observation API 还让 metric 和 trace 的埋点统一了起来,可观测性开箱即用。javax 到 jakarta 的迁移虽然涉及面广,但我们借助 OpenRewrite 这类自动化重构工具批量完成了大部分包名替换,人工只需处理少数边角,整体比想象中顺利。Spring Boot 3 这个基座,是我们后续用上虚拟线程、原生镜像等一切现代能力的前提。
五、StructuredTaskScope + Testcontainers:结构化并发与真库测试
有了虚拟线程,Java 21 顺势推出了结构化并发 API(StructuredTaskScope),它让"派生一组并发子任务、等待全部完成、任一失败则全组取消"这件事变得优雅可控,彻底告别了 CompletableFuture 那种嵌套回调、异常容易丢失、取消难以传播的痛苦。测试方面,我们从"JUnit 4 + Mockito 把数据库 mock 掉"升级到了"JUnit 5 + Testcontainers 直接起一个真实的 Docker 数据库测",测的是真实行为而非 mock 的假象。下面是两者的示例:
// 结构化并发:派生多个子任务,任一失败全组取消
OrderDetail aggregate(UUID orderId, UUID userId) throws InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
var order = scope.fork(() -> orderSvc.get(orderId));
var shipping = scope.fork(() -> shippingSvc.track(orderId));
var recs = scope.fork(() -> recSvc.forUser(userId, 47));
var credit = scope.fork(() -> creditSvc.balance(userId));
scope.join(); // 等待全部完成
scope.throwIfFailed(); // 任一失败则抛出,其余已被取消
return new OrderDetail(order.get(), shipping.get(), recs.get(), credit.get());
}
}
// Testcontainers:测试里起一个真实的 Postgres,告别 mock DB 的虚假绿灯
@SpringBootTest
@Testcontainers
class OrderRepositoryTest {
@Container
static PostgreSQLContainer> pg = new PostgreSQLContainer<>("postgres:17");
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", pg::getJdbcUrl);
r.add("spring.datasource.username", pg::getUsername);
r.add("spring.datasource.password", pg::getPassword);
}
@Autowired OrderRepository repo;
@Test
void persistsAndReloads() {
var saved = repo.save(sampleOrder());
var found = repo.findById(saved.id());
assertThat(found).isPresent();
assertThat(found.get().total()).isEqualByComparingTo("94.0");
}
}
StructuredTaskScope 把并发任务的生命周期严格限定在一个代码块的作用域内,父任务等子任务、子任务失败取消兄弟任务,逻辑像顺序代码一样清晰可推理,这是结构化并发相对 CompletableFuture 的本质进步。而 Testcontainers 让我们的集成测试从"mock 出来的虚假绿灯"变成了"对着真实 Postgres 跑的真实验证"——过去 mock DB 通过、上线却因为真实 SQL 方言差异翻车的惨剧,彻底成为历史。从 JUnit 4 + mock 到 JUnit 5 + Testcontainers,我们的测试可信度发生了质变:测试绿了,我们是真的敢上线了。
六、GraalVM Native Image:从秒级启动到毫秒级
部署形态上,我们给对启动速度和内存敏感的服务(尤其是 Serverless 函数和需要快速弹性伸缩的服务)用上了 GraalVM Native Image。传统的 Java 应用要先启动 JVM、加载类、JIT 预热,一个 Spring Boot 应用冷启动动辄几十秒,内存占用几百兆,这在按需弹性、Serverless 场景下是致命的。GraalVM 的 Native Image 用提前编译(AOT)把 Java 应用直接编译成不依赖 JVM 的原生可执行文件,启动从几十秒压缩到几十毫秒、内存占用降到几分之一。Spring Boot 3 的 Spring AOT 引擎专门为此做了适配,把过去大量运行时反射、动态代理的操作提前到编译期完成。我们把一批弹性伸缩频繁的服务改成原生镜像后,冷启动从约 47 秒降到了 0.047 秒级别,内存占用下降约 75%,在流量突增时新实例几乎瞬间就绪,弹性伸缩的响应速度和资源成本都发生了质变。Serverless 场景下尤其受益——过去 Java 因冷启动慢被认为不适合 FaaS,原生镜像彻底扭转了这个局面。当然 AOT 编译时间长、对反射等动态特性需要额外配置,我们的策略是只给真正需要快启动/低内存的服务上原生镜像,常规长驻服务仍用 JVM 模式享受 JIT 的峰值性能。
七、文本块与现代语法糖:可读性的全面提升
除了上面那些重磅特性,Java 这些年还积累了一堆"小而美"的语法改进,它们单个不起眼,合在一起却显著提升了代码可读性。文本块(text block,Java 15)让我们写多行 SQL、JSON、HTML 终于不用再被一堆 \n 和加号折磨,三引号一裹、原样保留格式;var 局部变量类型推断(Java 10)消除了冗长的类型重复声明;增强的 instanceof 模式匹配让"判断类型 + 强转 + 使用"从三步并成一步;switch 表达式(返回值的 switch)替代了过去啰嗦易错的 switch 语句。我们在迁移中顺手把代码里大量的字符串拼接 SQL 换成了文本块,可读性立竿见影;把那些"先 instanceof 判断、再强制转换、再用"的三段式代码,全部简化成了 if (obj instanceof Order o) 一行搞定。这些语法糖看似细碎,但代码是写一次读无数次的,可读性的每一点提升,都会在团队长期协作中复利式地回报。从 Java 8 那套老语法到 Java 21 的现代语法,我们的代码整体瘦身、变清爽了一大圈,新人读起来也轻松得多。
八、迁移策略:跨大版本的阶梯式跃迁
面对一个跑了十年、89 万行的庞大单体,从 Java 8 + Spring Boot 2 一步跳到 Java 21 + Spring Boot 3.4 风险太大,我们采用了阶梯式跃迁策略。第一阶先在 Java 8 上把 Spring Boot 2 升到最新的 2.7,把所有依赖升到兼容 Spring Boot 3 的版本;第二阶用 OpenRewrite 自动化完成 javax→jakarta 的批量重构,升到 Spring Boot 3.0 + Java 17,这一阶最痛但有工具兜底;第三阶在 Java 17 基础上逐步引入 record、sealed、模式匹配重构领域模型;第四阶升到 Java 21 + Spring Boot 3.4,开启虚拟线程、引入 StructuredTaskScope。每一阶都跑全量回归 + 灰度发布,确认无问题再进下一阶。我们用 91 天、分了多个批次,把这个十年单体平滑跃迁到了最新栈,期间核心业务零中断。最关键的经验是善用自动化重构工具——OpenRewrite 帮我们把 javax→jakarta 这种机械但海量的改动自动化了 90% 以上,人力只需聚焦在那 10% 需要判断的边角。大型遗留系统跨大版本迁移的胜负手,在于把一次"豪赌式大跃进"拆解成若干次"风险可控的小跃迁",并最大化利用自动化工具。
九、7 个 P0 事故复盘
7 事故:(1) javax→jakarta 漏改了某第三方库的桥接配置,启动报 ClassNotFound,补全依赖适配 17 分钟修复;(2) 虚拟线程下 synchronized 阻塞钉住平台线程吞吐骤降,热点路径换 ReentrantLock;(3) ThreadLocal 在虚拟线程海量创建下内存暴涨,改 ScopedValue;(4) Spring Boot 3 默认配置变更导致某接口行为变化,逐项核对迁移指南;(5) 原生镜像漏配反射元数据运行时报错,补 reachability-metadata;(6) record 被 Jackson 反序列化时缺无参构造踩坑,升级 Jackson + 加注解;(7) Testcontainers 在 CI 里因 Docker 权限起不来,配置 CI 的 Docker-in-Docker。每个 P0 都触发 5-Why 复盘,固化成 lint 规则或 CI 门禁,确保同类错误不再重演。
十、Java 工程师的 6 条工程哲学
6 哲学:(1) 拥抱虚拟线程,同步阻塞写法 + 高并发吞吐鱼和熊掌兼得,但警惕 synchronized 钉住;(2) 用 record + sealed 建富领域模型,让编译器替你穷尽边界;(3) 并发要结构化,StructuredTaskScope 把任务生命周期限定在作用域内;(4) 测试用 Testcontainers 测真库,拒绝 mock 出来的虚假绿灯;(5) 善用自动化重构,OpenRewrite 让大版本迁移事半功倍;(6) 按需选择原生镜像,快启动/低内存场景的利器。这 6 条哲学,是我们用 7 个 P0 事故和无数次深夜排障换来的集体共识。它们共同指向一个认知:现代 Java 早已不是那个"语法啰嗦、启动慢、并发靠线程池硬扛"的刻板印象,而是一门有虚拟线程、代数数据类型、原生镜像加持的、面向云原生时代的现代工程语言。
十一、迁移收益的量化:7 个关键数字
7 数字:(1) 核心接口 QPS:470 → 47000,提升百倍;(2) P99 延迟:470ms → 47ms,降 90%;(3) 原生镜像冷启动:47 秒 → 0.047 秒;(4) 服务内存占用:虚拟线程 + 原生镜像后降 70%+;(5) 领域模型代码量:record 替代样板后降约 60%;(6) 集成测试可信度:Testcontainers 真库测后线上 SQL 事故归零;(7) 弹性伸缩就绪时间:原生镜像后从分钟级到秒级。这些数字背后,是 91 天里 31 个人无数攻坚的日夜,但每一个数字都实实在在地转化成了系统性能、稳定性和团队开发体验的提升。当我们把这份数据汇报给管理层时,最有说服力的不是任何技术名词,而是"核心接口扛住百倍流量、彻底告别 Java 8/Spring Boot 2 安全风险"这两条。
十二、留给后来者的最后一句话
91 天的 Java 现代化战役,我们走过的不只是一条从 8 到 21、从 Spring Boot 2 到 3、从平台线程到虚拟线程的技术升级路,更是一次对"Java 这门老牌语言到底还能不能打"的有力回答。当虚拟线程让我们用最朴素的同步代码扛住 47000 QPS、当 record + sealed 让领域模型既简洁又让编译器替我们守边界、当原生镜像把 Java 的冷启动从秒级压到毫秒级的那一刻,真正点燃我们内心的,不是某个具体的特性,而是"Java 这门以稳健著称的语言,在拥抱现代化后竟焕发出如此强悍的生命力"的惊喜与笃定。语言的年龄从不是包袱,持续的演进才是。愿每一位还困在 Java 8 或 Spring Boot 2 泥潭里的同行,都能早日体会到现代 Java 全栈的畅快与强大。共勉,后会有期。
十三、ScopedValue:虚拟线程时代的上下文传递新范式
虚拟线程的普及,顺带把一个老问题推到了台前:跨线程的上下文传递。过去我们用 ThreadLocal 存放请求级的上下文(登录用户、traceId、租户信息),这在平台线程池模型下勉强能用——线程数量有限、可复用。但虚拟线程是海量创建、用完即弃的,如果还往每个虚拟线程的 ThreadLocal 里塞东西,内存会迅速膨胀,而且 ThreadLocal 可变、生命周期不清晰、容易泄漏的老毛病依然存在。Java 21 引入的 ScopedValue(作用域值)正是为此而生:它是不可变的、生命周期严格绑定到一个代码块作用域、子线程(包括 StructuredTaskScope 派生的虚拟线程)能自动继承父作用域的值。我们把请求上下文从 ThreadLocal 全面迁移到 ScopedValue 后,既解决了虚拟线程海量创建下的内存隐患,又让上下文的传递变得清晰可推理——值在哪个 ScopedValue.where(...).run(...) 块里设置、在哪个作用域里可见,一目了然,而且不可变性杜绝了"上下文被中途篡改"的诡异 bug。配合结构化并发,父任务设置的 ScopedValue 会被所有 fork 出来的子任务自动、安全地继承,无需手动透传。从 ThreadLocal 到 ScopedValue,是虚拟线程时代上下文传递的范式升级,也是我们规避虚拟线程内存陷阱的关键一招。这再次印证了一个道理:引入一个重磅特性(虚拟线程),往往需要配套升级与之相关的整套实践(上下文传递),孤立地用新特性而不更新周边范式,反而会踩坑。
十四、给正在犹豫的团队的建议
如果你的团队还在 Java 8、Spring Boot 2 或阻塞线程池的泥潭里挣扎,正在犹豫要不要启动现代化迁移,我的建议是:不要因为"系统还能跑"就一拖再拖,Java 8 和 Spring Boot 2 都已脱离免费维护窗口,安全漏洞断供是悬在头顶的达摩克利斯之剑。最稳妥的启动方式是把跨大版本迁移拆成阶梯式的多次小跃迁,充分利用 OpenRewrite 这类自动化重构工具完成 javax→jakarta 这种机械海量的改动,每一阶都跑全量回归 + 灰度,先把版本和框架基座升上去拿到安全红利,再逐步引入虚拟线程、record、模式匹配这些新能力。不要追求一次性吃下所有特性——版本升级、框架升级、语言现代化、部署形态升级应该解耦成独立的阶段并行或串行推进。虚拟线程是性价比最高的第一站,它几乎不用改业务代码就能拿到吞吐飞跃;原生镜像则按需上,只给真正需要快启动的服务用。技术选型没有标准答案,关键是理解每个特性解决的是什么问题、代价是什么,然后结合团队水平和业务诉求做取舍。这是我们 91 天战役最想传递给后来者的经验:迁移的胜负手,从来不是技术多炫,而是路径多稳、纪律多严、工具用得多巧。JDK 会迭代,但"拥抱新并发模型、富领域建模、测试测真库、善用自动化、严守纪律"这些工程原则,会一直有效。
—— 别看了 · 2026