跳到主内容

Spring Boot 3 升级避坑实录:从 JDK 17 到 21 ​

为什么现在必须升级 Spring Boot 3 ​

很多仍运行在 Spring Boot 2.x 的服务,表面上稳定,实际已经进入维护成本递增阶段:Spring Framework 5 的开源支持结束,大量生态库停止发布兼容补丁,JDK 8/11 也无法继续获得新的语言特性和性能优化。升级到 Spring Boot 3 + JDK 21,不只是把版本号改大,而是一次围绕 Jakarta EE 9+、Micrometer Tracing、Native Image 和虚拟线程的工程基线迁移。

典型收益可以概括为三类:第一,依赖和运行时进入长期支持区间;第二,JDK 21 的 ZGC、G1、虚拟线程和模式匹配降低并发服务成本;第三,Spring Boot 3 的 GraalVM AOT 能力为 Serverless、弹性扩容和低启动延迟场景提供新选择。

但升级不能直接修改 parent 版本后等待编译通过。实际项目中,最常见的阻塞点是 javax.* 包迁移、第三方中间件 Starter 不兼容、Spring Security 配置类失效、Lombok/ByteBuddy/Mockito 等字节码工具版本过低。建议按“依赖评估 → 编译改造 → 运行验证 → 性能观测 → 可选原生镜像”的路径推进。

依赖盘点确认 Boot 3 版本编译改造javax 转 jakarta配置迁移Security/Actuator回归验证契约与压测虚拟线程试点IO 密集接口Native 可选验证AOT 与反射元数据

升级方案对比:一次性大版本还是分层迁移 ​

常见方案有两种。第一种是直接从 Spring Boot 2.7 跳到 3.2/3.3,并同步切换 JDK 21。优点是链路短,适合依赖简单、自动化测试完善的新服务;缺点是编译错误和运行时问题会集中暴露。第二种是先在 JDK 17 上升级到 Spring Boot 3.x,完成 Jakarta 包和第三方库替换,再单独切换 JDK 21 并验证虚拟线程。对于中大型业务系统,我更推荐第二种,因为它能把“框架行为变化”和“JDK 运行时变化”拆开定位。

如果当前仍在 Spring Boot 2.4 以下,建议先升到 2.7.x。2.7 同时支持较新的依赖坐标和迁移检查工具,可以利用 spring-boot-properties-migrator 先清理废弃配置,再进入 Boot 3。不要把多个跨度的升级塞进一个提交,否则回滚边界会非常模糊。

第一步:统一 Maven 依赖与 JDK 21 编译参数 ​

下面是一份可直接运行的最小 pom.xml,使用 Spring Boot 3.3.x、Java 21,并显式加入虚拟线程所需的 Web 与 Actuator 依赖。

xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.5</version>
    <relativePath/>
  </parent>

  <groupId>com.example
  <artifactId>boot3-jdk21-demo</artifactId>
  <version>1.0.0</version>

  <properties>
    <java.version>21</java.version>
    <maven.compiler.release>21</maven.compiler.release>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- 虚拟线程压测时观察 JVM、线程和内存指标 -->
    <dependency>
      <groupId>io.micrometer</groupId>
      <artifactId>micrometer-registry-prometheus</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>
</project>

升级时优先让 Spring Boot parent 管理常用依赖版本,不要在业务模块里手动锁一堆旧版本。对内部二方库,需要检查其是否直接引用 javax.servlet、javax.persistence、javax.validation。只要这些类出现在公共方法签名里,调用方升级后就可能出现 NoClassDefFoundError。

第二步:完成 Jakarta EE 包名迁移 ​

Spring Boot 3 基于 Spring Framework 6,最低要求 Jakarta EE 9+。包名规则非常机械:javax.servlet 变为 jakarta.servlet,javax.persistence 变为 jakarta.persistence,javax.validation 变为 jakarta.validation。注意不是所有 javax.* 都迁移,例如 javax.crypto、javax.net.ssl 仍属于 JDK,不能批量替换。

推荐使用 OpenRewrite 或 IDE 的结构化替换,先改源码,再通过 mvn dependency:tree 检查残留依赖。一个标准 Controller 应写成如下形式:

java
package com.example.demo;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
class OrderController {

    @PostMapping("/api/orders")
    @ResponseStatus(HttpStatus.ACCEPTED)
    OrderResponse create(@Valid @RequestBody CreateOrderRequest request) {
        // 这里只演示 Boot 3 + Jakarta Validation 的入口签名
        return new OrderResponse(System.currentTimeMillis(), request.itemName());
    }

    record CreateOrderRequest(@NotBlank String itemName, int quantity) {}

    record OrderResponse(long acceptedAt, String itemName) {}
}

如果项目使用 MyBatis、Quartz、Druid、Swagger、MapStruct、Lombok 等生态组件,要提前确认兼容版本。例如 Springfox 基本不再适配 Boot 3,应迁移到 springdoc-openapi-starter-webmvc-ui;Hibernate 5 升级到 Hibernate 6 后,SQL 函数、隐式关联和日期类型处理都可能变化,必须对核心 SQL 做执行计划和结果集对比。

第三步:迁移配置并启用 JDK 21 虚拟线程 ​

Spring Boot 3 可以通过一个配置开启虚拟线程。它适合 Tomcat 请求处理、JDBC 阻塞调用、HTTP Client、Redis 访问等 IO 密集场景,但不适合用 synchronized 长时间包住外部调用,因为 JDK 21 中这种模式仍可能造成载体线程钉住。

yaml
server:
  port: 8080

spring:
  threads:
    virtual:
      enabled: true   # Tomcat 请求处理与部分异步任务使用虚拟线程

management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      show-details: when_authorized

启动类保持简洁即可:

java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

如果要确认请求是否跑在虚拟线程上,可以临时增加一个调试接口:

java
package com.example.demo;

import java.time.Duration;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ThreadInspectController {

    @GetMapping("/api/thread")
    String currentThread() throws InterruptedException {
        // 模拟一次下游 IO 等待,压测时观察线程数与内存占用
        Thread.sleep(Duration.ofMillis(100));
        Thread current = Thread.currentThread();
        return "thread=%s, virtual=%s%n".formatted(current.getName(), current.isVirtual());
    }
}

在 IO 等待较多的接口上,虚拟线程通常能显著降低平台线程数量,提升并发承载。但数据库连接池仍是硬上限,不要误以为开启虚拟线程后就能无限访问数据库。连接池、限流和下游超时仍要按容量配置。

第四步:处理 Spring Security 与可观测性变化 ​

Spring Security 6 大量使用 Lambda DSL,旧的 authorizeRequests()、WebSecurityConfigurerAdapter 已移除。最小配置如下:

java
package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration(proxyBeanMethods = false)
class SecurityConfig {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/actuator/health", "/api/orders").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}

链路追踪方面,Spring Cloud Sleuth 不再适配 Boot 3,应迁移到 Micrometer Tracing,并根据后端选择 OpenTelemetry 或 Brave。升级后要核对 traceId 是否透传、日志格式是否变化、异步线程池和定时任务是否会丢上下文。

可选验证:GraalVM Native Image 的边界 ​

Spring Boot 3 的 AOT 和 GraalVM Native Image 能把启动时间从秒级压缩到几十毫秒级,内存峰值也可能下降,适合函数计算、CLI 和短时任务。但普通业务服务不要把它当作升级必选项。反射、动态代理、资源加载、SPI、表达式引擎和 CGLIB 都可能需要额外 reflect-config.json、resource-config.json 或 @RegisterReflectionForBinding。

如果要试验,可增加 native profile 后执行:

bash
mvn -Pnative native:compile
./target/boot3-jdk21-demo

验证重点不是“能否打包”,而是启动后关键接口、JSON 序列化、数据库驱动、缓存客户端和配置解密是否全部可用。Native Image 的编译时间长、构建环境要求高,建议先在独立服务试点,不要在核心交易链路上全量铺开。

实测收益与上线建议 ​

在一个以 HTTP 聚合、Redis 和 JDBC 查询为主的后端服务中,我们先完成 Boot 3.3 + JDK 17 迁移,再切换 JDK 21 并开启虚拟线程。接口平均响应时间变化不大,但 800 并发下 Tomcat 工作线程从约 200 个平台线程降低为少量载体线程加大量虚拟线程,扩容时冷启动和线程调度压力更小。Native 试点服务启动时间约从 2.4 秒降到 0.08 秒,但构建时间明显增加,因此只用于弹性任务节点。

上线时建议按流量灰度:先关闭虚拟线程发布 Boot 3 版本,观察一周 GC、类加载、SQL 耗时和错误率;再开启虚拟线程发布第二版本;最后选择低风险服务验证 Native。这样每次发布都能独立回滚,避免把收益和风险绑在一起。

避坑总结 ​

  1. 不要全量机械替换 javax.*。错误现象是编译期提示 package javax.crypto does not exist 或安全工具类异常。修正方法是只迁移 Jakarta 管理的 Servlet、Validation、Persistence、Annotation 等包,JDK 自带加密、网络包保持 javax.* 不变。

  2. 不要只改 Spring Boot parent,忽略字节码工具。典型报错是 Unsupported class file major version 65、Lombok 无法生成 getter、Mockito 无法 mock final 类。应升级 Lombok 1.18.30+、ByteBuddy 1.14+、Mockito 5.x、JaCoCo 0.8.11+,并让插件版本与 JDK 21 对齐。

  3. 虚拟线程不是无限并发开关。错误现象是开启后数据库连接等待升高、接口超时增加、CPU 没打满但 RT 变长。原因通常是请求并发远大于连接池容量。修正方法是保留 HikariCP 最大连接数、下游线程隔离和信号量限流,并把长时间 synchronized 块改为 ReentrantLock 或缩小临界区。

  4. Spring Security 配置不能照搬旧代码。常见异常是启动时报 WebSecurityConfigurerAdapter 找不到,或所有接口意外返回 401。应使用 SecurityFilterChain Bean,明确放行 actuator、登录和内部回调路径,并通过集成测试覆盖未登录、已登录、越权三类请求。

  5. Native Image 下动态能力最容易运行时才失败。现象是 JVM 模式正常,原生镜像启动后 JSON 反序列化报 ClassNotFoundException,或资源文件读取为空。修正方法是为 DTO、枚举和动态加载类补充反射注册,把资源路径加入 AOT 配置,并在 CI 中直接运行原生产物的契约测试。

总体来看,Spring Boot 3 + JDK 21 的升级收益明确,但它更像一次工程基线治理。只要控制好依赖兼容、Jakarta 迁移、安全配置和虚拟线程边界,再用灰度发布与可观测指标兜底,就能把一次看似高风险的大版本升级,拆成可验证、可回滚、可持续的平台演进。

免责声明

本工具与文章内容仅用于学习测试场景,优化建议仅供参考,请勿直接用于生产环境,由此产生的风险自行承担。