Java 17+ 与 Spring Boot 3.x:原理、版本边界和迁移全过程
Spring Boot 2.7 + JDK 8 仍然是大量企业存量系统的重要组合,但新系统不能永远停留在这条基线上。Spring Boot 3 建立在 Spring Framework 6 和 Java 17 之上,并迁移到 Jakarta EE 命名空间,同时增强了 AOT、原生镜像、可观测性和云原生运行能力。
这一页采用“双轨学习”方式:既讲清旧项目为什么还能运行,也讲清新项目为什么应该评估 Java 17+ 与 Boot 3.x。重点不是背一张差异表,而是理解每项变化发生在哪一层、为什么必须改、不改会出现什么错误、怎样用证据完成迁移。
一、学完后必须具备什么能力
- 区分 JDK、Java 语言级别、JVM、Spring Framework、Spring Boot 和 Jakarta EE 的职责。
- 说明为什么 Spring Boot 3 的最低 Java 基线是 17,而不是 8 或 11。
- 说明
javax.*为什么迁移到jakarta.*,以及为什么不能只做字符串替换。 - 正确区分 Boot 2.6、2.7、3.0、3.2 等代际,不把某个小版本能力说成整个 Boot 3 都有。
- 分别创建可运行的 Boot 2.7/JDK 8 和 Boot 3.x/Java 17 Web 服务。
- 看懂自动配置候选机制、构造器绑定、Servlet 容器、参数校验、Security、Observability 的变化。
- 设计一条可回滚、可验证、可灰度的商业系统迁移流程。
- 根据异常类型判断问题来自字节码、包名、依赖二进制兼容、配置迁移还是运行环境。
本章是版本迁移总入口。需要继续深入时按下面的学习顺序进入专题页:
- Java 17 现代基线:语言、JVM、强封装与升级
- Spring Boot 3.x 内部执行链:启动、自动配置、Jakarta、AOT 与观测
- Java 17 与 Spring Boot 3.x 独立面试题
二、双轨版本观:旧基线与新基线不是二选一
| 轨道 | 常见组合 | 主要用途 | 关键约束 |
|---|---|---|---|
| 历史维护 | JDK 7 + 传统 Spring 或很早期 Boot | 只用于接手历史系统 | 生态停止演进、安全和运维成本高,不适合新建 |
| 企业存量 | JDK 8 + Spring Boot 2.7.x | 维护仍有生命周期治理的存量服务 | 使用 Spring 5.3、javax.*;升级依赖时必须检查兼容性 |
| 现代基线 | Java 17+ + Spring Boot 3.x | 新项目与现代化升级 | Spring 6、Jakarta 命名空间、依赖必须整体兼容 |
| 进一步演进 | Java 21 + 支持它的 Boot 3.x 小版本 | 需要虚拟线程等新能力 | Java 21 能力不能错误地写成 Java 17 能力 |
选择版本时必须查目标 Spring Boot 精确版本的系统要求和依赖清单。3.x 是一个持续演进的系列,不代表所有 3.x 小版本支持完全相同的 JDK 上限、Gradle、Maven、Tomcat 或第三方组件。
flowchart TD
A["确认是存量维护还是新建系统"] --> B["记录 JDK、Boot、Cloud 精确版本"]
B --> C["检查支持周期与依赖兼容矩阵"]
C --> D["存量系统制定分阶段迁移<br/>新系统选择受支持的现代基线"]
D --> E["锁定 BOM 与构建 JDK"]
E --> F["建立可重复构建和回滚基线"]存量系统与新系统在第四步采取不同决策:存量系统先评估并分阶段升级,新系统优先选择仍受支持的 Java LTS 与匹配的 Boot 版本;流程图纵向合并这两个结果是为了保证窄屏可读。
为什么不能“所有服务直接升最新版”
微服务系统的依赖不只有 Boot。还包括 Spring Cloud 发布列车、Spring Cloud Alibaba、注册中心客户端、数据库驱动、ORM、MQ、APM Agent、日志实现、Servlet Filter、公司公共 Starter。任何一个 Jar 若仍依赖旧 javax 类型,都可能在启动或请求执行时失败。
为什么也不能“旧系统能跑就永远不升”
长期停留在旧基线会累积 JDK 安全补丁、框架维护、容器镜像、TLS 算法、依赖漏洞和人才维护成本。正确方式是把升级当作工程项目:先盘点,再补测试,再迁依赖,再灰度,而不是无限拖延或一次性豪赌。
三、先分清变化发生在哪一层
| 层 | 负责什么 | Boot 3 相关变化示例 |
|---|---|---|
| Java 语言 | 语法和标准库 API | 文本块、模式匹配、Record、密封类;具体能力受 Java 版本限制 |
| JVM / class 文件 | 加载、验证、JIT、GC | Boot 3 运行时最低要求 Java 17;低版本 JVM 无法加载高版本字节码 |
| Spring Framework 6 | IoC、AOP、事务、MVC、WebFlux | Java 17 基线、Jakarta EE 9+、AOT 基础、HTTP 接口等 |
| Spring Boot 3 | 依赖管理、自动配置、启动、Actuator | Boot 3 自动配置注册、Observability、原生镜像支持等 |
| Jakarta EE / Servlet | Web、验证、持久化等规范 API | javax.servlet 迁到 jakarta.servlet,容器也必须实现新规范 |
| Spring Cloud | 微服务组件集成 | 必须选择与 Boot 3 精确兼容的 Release Train |
| 第三方生态 | ORM、驱动、Agent、Starter | 每个组件都必须提供 Spring 6/Jakarta 兼容版本 |
一个 ClassNotFoundException 可能不是 Boot 的启动流程错了,而是第三方 Jar 仍在找 javax.servlet.Filter;一个 UnsupportedClassVersionError 也不是 Spring Bean 错了,而是运行 JVM 低于编译字节码版本。排查前必须先定位层次。
四、为什么 Spring Boot 3 以 Java 17 为最低基线
Spring Boot 3 基于 Spring Framework 6,而 Spring Framework 6 将 Java 17 设为最低基线。因此 Boot 3 应用无法在 JDK 8 或 11 JVM 上运行。这是运行时硬约束,不是“建议使用”。
4.1 基线提升给框架带来什么
框架不再需要保留大量旧 JDK 兼容分支,可以使用更现代的 JVM、语言和标准库能力,并把维护和测试资源集中在受支持的平台上。应用也能获得 Java 9 之后持续积累的 JVM、GC、容器识别、诊断和语言能力。
4.2 编译 JDK、--release 和运行 JDK不是一回事
| 概念 | 含义 | 常见错误 |
|---|---|---|
| 编译 JDK | 执行 javac 的 JDK | 本机 IDE 用 17,CI 却仍用 8 |
--release | 限制目标 class 版本和可见标准库 API | 只改 source/target,仍误用高版本标准库 |
| 运行 JDK | 真正执行 Jar 的 JVM | 编译成功,部署后 UnsupportedClassVersionError |
| Maven Toolchains | 为构建插件显式选择 JDK | Maven 自身和编译器使用了不同 JDK |
Java 17 编译出的默认 class major version 是 61,Java 8 是 52。Java 8 JVM 不认识版本 61 的 class 文件。
flowchart TD
A["Javac生成class文件"] --> B["class头部携带major version"]
B --> C["目标JVM读取并校验"]
C --> D["比较class版本与JVM支持上限"]
D --> E["支持则继续验证和链接<br/>不支持则抛出版本错误"]4.3 Java 17 可以用哪些现代能力
- Record 适合表达不可变数据载体,但不要机械替换 JPA Entity。
- 文本块适合长 SQL、JSON 和测试数据。
instanceof模式匹配减少重复强制转换。- 密封类可限制领域状态或结果类型的实现集合。
- 更现代的 GC 和 JVM 诊断能力有利于生产运行。
这些能力是可选的业务代码改进,不是 Boot 3 能启动的原因。Boot 3 能否运行取决于框架和 JVM 基线,不能说“因为用了 Record 所以必须 Boot 3”。
4.4 Java 21 能力必须单独标记
虚拟线程是 Java 21 正式能力,不是 Java 17 能力。某些 Boot 3.x 小版本提供虚拟线程相关配置支持,但必须同时满足:
- 运行时确实是 Java 21 或更高兼容版本。
- Boot 小版本支持相应集成。
- 依赖库没有大量
synchronized固定、ThreadLocal 滥用或不适合的线程亲和假设。 - 数据库连接池等稀缺资源仍要限流;虚拟线程不会凭空增加数据库连接。
五、javax.* 到 jakarta.*:为什么是二进制不兼容迁移
Java EE 移交 Eclipse 基金会后演进为 Jakarta EE。Jakarta EE 9 将大量规范 API 的包名从 javax.* 改成 jakarta.*。Spring Framework 6 面向 Jakarta 命名空间,因此 Boot 3 Web 应用也进入新的类型体系。
5.1 常见包名变化
| Boot 2 / Spring 5 | Boot 3 / Spring 6 |
|---|---|
javax.servlet.* | jakarta.servlet.* |
javax.validation.* | jakarta.validation.* |
javax.persistence.* | jakarta.persistence.* |
javax.annotation.* | jakarta.annotation.* |
javax.transaction.* | jakarta.transaction.* |
注意:不是所有 javax.* 都迁移。例如 Java SE 自带的 javax.crypto、javax.net、javax.sql 不应批量替换。它们属于 Java SE,不属于此次 Jakarta EE 包迁移范围。
5.2 为什么改一个 import 还不够
JVM 用完全限定类名识别类型。javax.servlet.Filter 和 jakarta.servlet.Filter 是两个不同的类型,即使方法看起来相同也不能互相赋值。旧 Filter Jar 编译后的常量池仍然引用 javax/servlet/Filter,改业务源码不会重写第三方 Jar。
flowchart TD
A["Boot 3启动并加载组件"] --> B["JVM解析第三方Jar常量池"]
B --> C["检查规范类型是 jakarta 还是旧 javax"]
C --> D["Jakarta兼容则继续创建组件"]
D --> E["旧类型失败时定位来源Jar"]
E --> F["升级、替换或重新编译组件"]第三步的真实结果是二选一:兼容组件继续启动;仍引用旧类型的组件会在类加载、方法链接或类型转换阶段失败。图中将失败处理画在主链下方,便于展示完整迁移处置路径。
5.3 常见失败表现
ClassNotFoundException: javax.servlet.Filter:旧库仍需要旧 Servlet API。NoClassDefFoundError: javax/validation/...:验证库或自定义 Starter 未迁移。NoSuchMethodError:编译期和运行期依赖版本不一致。- Bean 类型不匹配:接口一边是
javax,实现一边是jakarta。 - 应用启动成功但请求失败:某个 Filter、序列化器或懒加载路径到请求时才触发。
5.4 Servlet 容器为什么也要一起升级
Boot 2.7 常见 Tomcat 9,面向旧 javax.servlet 命名空间;Boot 3 常见 Tomcat 10.1,面向 Jakarta Servlet 6.0。不能把旧 Tomcat 9 当作 Boot 3 的外部容器,也不能认为“都是 Tomcat 所以兼容”。
六、Spring Boot 3 核心变化及其原理
6.1 自动配置候选类注册机制
常见说法“Boot 2 用 spring.factories,Boot 3 用 AutoConfiguration.imports”方向大致正确,但版本边界需要更精确:
| 版本代际 | 自动配置候选注册 |
|---|---|
| Boot 2.6 及更早常见版本 | META-INF/spring.factories 的 EnableAutoConfiguration key |
| Boot 2.7 | 已支持并推荐 AutoConfiguration.imports,旧方式仍可兼容 |
| Boot 3 | 使用 AutoConfiguration.imports 注册自动配置;旧 spring.factories 自动配置候选机制已移除 |
Boot 3 文件位置:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports内容每行一个自动配置类:
com.example.collect.autoconfigure.CollectClientAutoConfiguration这不表示 spring.factories 文件在 Boot 3 世界里完全不存在;Spring/Boot 的其他工厂扩展仍可能使用它。被移除的是通过旧 key 注册自动配置候选类的方式,回答时要限定语境。
自动配置的根本原理没有变化:候选类被导入,条件注解根据 classpath、配置、Web 类型和已有 BeanDefinition 做判断,匹配后注册 BeanDefinition,最后仍由 Spring IoC 在 refresh() 中创建 Bean。
6.2 @ConfigurationProperties 与构造器绑定
Boot 2.2 到 2.7 的不可变属性类常在类型上标注 @ConstructorBinding。Boot 3 中,只有一个参数化构造器时会推断构造器绑定,通常不再需要在类型上标注;存在多个构造器时才需要明确选择绑定构造器,并注意注解位置和包名变化。
不要看到 Boot 3 就把所有属性类改成 Record。Record 很适合不可变配置,但嵌套结构、默认值、校验、元数据生成和团队可读性仍需评估。
6.3 Spring MVC 与 Servlet 栈
请求主链并没有因为 Boot 3 而消失:
flowchart TD
A["客户端请求"] --> B["Tomcat 10.1连接器"]
B --> C["jakarta.servlet.Filter链"]
C --> D["DispatcherServlet"]
D --> E["HandlerMapping定位Controller"]
E --> F["参数解析、转换和jakarta.validation校验"]
F --> G["Controller与Service"]
G --> H["消息转换器序列化响应"]变化的是规范类型和组件版本,不是 MVC 的核心职责。迁移时如果只验证“应用端口起来了”,没有覆盖 Filter、参数校验、文件上传、异常处理和序列化,就无法证明迁移成功。
6.4 Spring Security 6 集成变化
Boot 3 对应 Spring Security 6。旧式继承 WebSecurityConfigurerAdapter 的配置方式已退出主线,推荐声明 SecurityFilterChain Bean;授权匹配 API 也应按 Security 6 的方式迁移。
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
}迁移不能只求编译通过,还要验证匹配顺序、默认安全行为、CSRF、CORS、Session/JWT、异常响应和方法级授权。安全规则变化属于业务风险,不只是 API 替换。
6.5 Observability:指标、Trace 和日志关联
Boot 3 的可观测性主线建立在 Micrometer Observation 上。Spring Cloud Sleuth 不再是新代际主线,分布式追踪通常迁移到 Micrometer Tracing,并通过 OpenTelemetry 或 Brave 等桥接实现传播和上报。
一次 HTTP、数据库或消息调用可抽象为 Observation:
flowchart TD
A["业务操作开始"] --> B["创建Observation上下文"]
B --> C["计时并写入低基数标签"]
C --> D["Tracing Handler创建或关联Span"]
D --> E["跨进程传播trace上下文"]
E --> F["结束Observation并上报指标与Trace"]必须控制标签基数。把 userId、订单号、完整 URL 放进指标标签,会制造海量时间序列;这些高基数值更适合日志或 Trace 属性。
6.6 AOT 与 Native Image 为什么需要“闭世界”信息
传统 JVM 可以在运行时扫描 classpath、反射加载类和生成代理。原生镜像在构建期尽量确定可达代码,运行时动态性受到更强约束,因此框架需要提前生成初始化代码,并为反射、资源、序列化和代理提供 Runtime Hints。
flowchart TD
A["应用源码和依赖"] --> B["Spring AOT分析Bean与配置"]
B --> C["生成初始化代码和Runtime Hints"]
C --> D["Native Image闭世界分析"]
D --> E["生成原生可执行文件"]
E --> F["更快启动和较低空闲内存"]原生镜像不是“全面比 JVM 快”。它常在启动速度和空闲内存上有优势,但构建更慢、动态特性需要声明、峰值吞吐和调试方式要实测。长时间运行且 JIT 充分预热的服务,不一定适合直接改成 Native。
6.7 HTTP 客户端能力要按小版本区分
WebClient来自 Spring WebFlux,可用于响应式或非阻塞链路,并非 Boot 3 新增。- HTTP Interface 在 Spring Framework 6 提供声明式接口能力。
RestClient属于 Spring Framework 6.1 / Boot 3.2 代际,不应声称所有 Boot 3.0 项目都有。RestTemplate并非一升级 Boot 3 就立即不能运行,但新同步客户端设计应评估RestClient的版本前提。
七、依赖管理:为什么代码没改也可能启动失败
Spring Boot BOM 管理一组经过协同测试的依赖版本。业务项目手工覆盖 Jackson、Netty、Hibernate、Logback 或 Tomcat 版本,可能破坏这组约束,引发 NoSuchMethodError、序列化差异或启动异常。
7.1 正确依赖顺序
- 选择 Java 运行基线。
- 选择受支持的 Boot 精确版本。
- 按官方兼容矩阵选择 Spring Cloud Release Train。
- 再选择匹配的 Spring Cloud Alibaba、数据库、MQ 和公司 Starter。
- 用 BOM 管理版本,少量覆盖必须记录原因和验证证据。
7.2 依赖树应该查什么
mvn dependency:tree
mvn dependency:tree -Dverbose
mvn help:effective-pom重点寻找:同一组件多版本、旧 javax API、被手工覆盖的 Spring Framework、旧 Hibernate Validator、旧 Servlet Filter、旧 APM Agent。
Gradle 项目可以使用:
./gradlew dependencies
./gradlew dependencyInsight --dependency spring-core八、双版本可运行 Demo
下面两个 Demo 提供相同的订单查询接口,便于直接观察版本差异。不要把两个版本的源码和依赖混在同一个模块中;真实迁移应使用两个分支或独立验证模块。
8.1 JDK 8 + Spring Boot 2.7.18
pom.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>2.7.18</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>order-boot2-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>8</java.version>
</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-validation</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Boot 2 DTO 使用 javax.validation:
package com.example.order;
import javax.validation.constraints.NotBlank;
public class CreateOrderRequest {
@NotBlank
private String productCode;
public String getProductCode() {
return productCode;
}
public void setProductCode(String productCode) {
this.productCode = productCode;
}
}8.2 Java 17 + Spring Boot 3.x
示例中的 Boot 版本应替换为团队当前验证并受支持的精确 3.x 版本,不能把文档示例当作永久版本矩阵:
<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.13</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>order-boot3-demo</artifactId>
<version>1.0.0</version>
<properties>
<java.version>17</java.version>
</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-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>Java 17 启动类:
package com.example.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}Boot 3 DTO 使用 jakarta.validation,这里使用 Record 表达不可变输入:
package com.example.order;
import jakarta.validation.constraints.NotBlank;
public record CreateOrderRequest(
@NotBlank(message = "productCode不能为空") String productCode) {
}Controller:
package com.example.order;
import jakarta.validation.Valid;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderResponse create(@Valid @RequestBody CreateOrderRequest request) {
return new OrderResponse(UUID.randomUUID().toString(), request.productCode(), "CREATED");
}
public record OrderResponse(String orderId, String productCode, String status) {
}
}运行与验证:
mvn clean verify
mvn spring-boot:run
curl -i -X POST http://localhost:8080/api/orders \
-H "Content-Type: application/json" \
-d '{"productCode":"SKU-1001"}'空 productCode 应返回 400;正常输入应返回 201。只验证 200/201 不够,迁移测试还应覆盖校验失败、JSON 字段错误、异常转换和安全过滤器。
九、迁移第一阶段:建立可证明的依赖与行为清单
升级前先回答以下问题:
- 当前生产实际运行 JDK 是多少,而不是开发电脑是多少?
- Boot、Spring Framework、Cloud、Cloud Alibaba 的精确版本是什么?
- 哪些公共 Starter、Filter、Interceptor、JPA Entity、Validator 使用
javax? - 是否依赖 Sleuth、旧 Security 配置、旧 Hibernate、自定义序列化器?
- 构建插件、测试插件、代码生成器和 APM Agent 是否支持 Java 17?
- 当前 API、消息、数据库行为基线是什么?
建议保存以下证据:
java -version
mvn -version
mvn dependency:tree > dependency-tree-before.txt
mvn help:effective-pom > effective-pom-before.xml再建立契约测试:HTTP 状态码与 JSON、MQ 事件 Schema、数据库写入、事务回滚、权限规则、定时任务、序列化兼容。没有行为基线,升级后“能启动”也无法证明业务没变。
十、Boot 2.7 到 Boot 3.x 的可回滚迁移流程
flowchart TD
A["固定当前行为基线"] --> B["先升级到Boot 2.7最新维护基线"]
B --> C["清理废弃API和手工覆盖依赖"]
C --> D["切换构建与运行JDK到17"]
D --> E["升级Boot 3与Spring 6兼容依赖"]
E --> F["迁移javax到jakarta及框架API"]
F --> G["运行单测、集成测试和契约测试"]
G --> H["预发布压测与可观测性对比"]
H --> I["小流量灰度并保留回滚版本"]10.1 为什么建议先站到 Boot 2.7
从更老版本直接跨越多个代际,会把配置变化、废弃 API、依赖升级和 Jakarta 迁移混在一次提交里。先迁到 2.7 可以消除一部分旧债,并利用 2.7 对新自动配置机制等过渡能力的支持。
10.2 为什么先让应用在 Java 17 上跑,再切 Boot 3
Boot 2.7 可以运行在 Java 17 上。先只改变运行时,有助于把 JDK 变化和框架变化拆成两个可观察步骤。若同时改变 JDK、Boot、Cloud、ORM 和容器,异常很难归因。
10.3 使用迁移报告和自动化工具,但不能盲信
Spring Boot 提供 properties migrator 帮助发现一部分重命名或删除的配置;OpenRewrite 等工具可以机械迁移常见 API。工具只能减少重复劳动,不能验证权限语义、事务边界、SQL、消息兼容和线上容量。
10.4 配置项如何迁移
- 启动时收集废弃与未绑定配置警告。
- 对比 Boot 2 和目标 Boot 3 的配置元数据。
- 检查 Actuator
/env、/configprops和/conditions。 - 注意端点暴露和脱敏,不能把密码公开给生产网络。
- 删除 migrator 前重新跑一次全量测试,避免长期依赖兼容层。
十一、微服务升级不能只升级单个应用
11.1 Spring Cloud 必须按发布列车匹配
Spring Cloud 不是随便选一个版本号。每个 Release Train 对应一组受支持的 Boot 版本。若 Boot 3 升级后仍保留不兼容的旧 Cloud,可能出现自动配置类缺失、方法签名错误、负载均衡器或配置加载异常。
11.2 Spring Cloud Alibaba 也有第三层矩阵
必须同时对齐:
Java → Spring Boot → Spring Cloud Release Train → Spring Cloud Alibaba → Nacos/Sentinel/Seata等客户端不能只把 Nacos Starter 的版本改大。配置加载方式、服务发现模型、负载均衡、Sentinel 适配和 Seata 数据源代理都可能受影响。
11.3 服务可以分批升级吗
通常可以,因为 HTTP、RPC、MQ 和数据库协议跨 JVM 版本。但必须保证:
- HTTP 契约和 JSON 字段兼容。
- Dubbo 等 RPC 协议、序列化和接口模型兼容。
- 消息消费者能处理新旧 Schema。
- Trace 传播头在新旧观测体系间可衔接。
- 注册中心元数据和健康检查语义一致。
- 回滚时数据库 Schema 仍向后兼容。
先升级边缘、低风险、依赖少的服务,再处理核心交易链;也可以先升级公共基础设施验证服务,但不要让未经验证的新公共 Starter 一次污染所有服务。
十二、商业系统验收:不只是“构建成功”
| 验收层 | 必测内容 | 证据 |
|---|---|---|
| 构建 | Java版本、依赖收敛、重复类 | CI日志、dependency tree |
| 启动 | Profile、配置绑定、自动配置、端口 | 启动日志、conditions |
| Web | Filter、校验、JSON、上传、异常 | MockMvc/真实HTTP测试 |
| 安全 | 登录、授权、CSRF/CORS、拒绝响应 | 安全契约测试 |
| 数据 | SQL、事务、锁、ORM映射 | 集成测试和数据库证据 |
| 消息 | 序列化、重复、顺序、重试、死信 | Broker与消费日志 |
| 微服务 | 注册、发现、负载、超时、熔断 | Trace和故障注入 |
| 性能 | 吞吐、P95/P99、CPU、Heap、GC | 对照压测报告 |
| 运维 | 健康探针、优雅停机、Dump、回滚 | 发布演练记录 |
十三、升级失败的证据化排查 Runbook
13.1 第一步:确认真正使用的 Java
java -version
mvn -version
java -XshowSettings:properties -version容器中还应执行同样命令,不要用宿主机结果替代容器结果。检查镜像 tag 之外,还要看镜像 digest 和进程实际二进制。
13.2 UnsupportedClassVersionError
含义:运行 JVM 不支持某个 class 的 major version。
排查:确定报错 class 来自业务还是依赖;检查 CI 编译 JDK、Maven Compiler release、运行镜像。解决方式是升级运行 JVM,或按目标版本重新编译;Boot 3 本身不能降到 Java 8 class 级别运行。
13.3 ClassNotFoundException: javax...
含义:仍有代码或 Jar 依赖旧 Jakarta EE 前身类型。
排查:
mvn dependency:tree
jdeps --multi-release 17 your-library.jar再检查异常栈中第一个业务或第三方类。不要简单补一个旧 javax.servlet-api 到 Boot 3 中让两套 Servlet API 共存,这通常会把问题推迟成更隐蔽的类型冲突。
13.4 NoSuchMethodError 或 AbstractMethodError
含义:编译时看到的方法与运行时实际加载的类不一致,典型原因是依赖冲突或旧扩展实现不兼容新接口。
排查:用 dependency:tree -Dverbose 找版本覆盖;用 -verbose:class 或 Java 9+ 的 -Xlog:class+load=info 查看类从哪个 Jar 加载;恢复 Boot BOM 管理并升级第三方扩展。
13.5 应用启动但接口 404/500
404 要检查组件扫描、路径匹配、Context Path 和 Security;500 要沿异常链检查 Filter、参数校验、Jackson、ORM 和业务调用。迁移后 javax.validation 注解若没有进入新的校验体系,可能出现“没有报错但校验没生效”,因此必须有非法输入测试。
13.6 Actuator 或 Trace 消失
先检查 Actuator 依赖与端点暴露,再检查 Micrometer Observation/Tracing bridge、Exporter、采样率和传播格式。不要因为日志中有 traceId 就认定 Trace 已成功上报。
13.7 启动内存和延迟变化
对比同等流量、相同容器限制下的 CPU、RSS、Heap、Metaspace、GC、线程数、连接池和 P99。升级 JDK 后默认 GC 或容器参数可能不同;只比较平均响应时间会掩盖尾延迟。
十四、常见错误,以及“不这样会怎样”
| 错误 | 为什么错 | 后果 |
|---|---|---|
只替换 javax 字符串 | 第三方字节码和容器规范不会随源码替换 | 启动或请求期类加载失败 |
| Boot 升 3,Cloud 版本不动 | 发布列车不兼容 | 自动配置或调用链运行失败 |
| 手工指定所有 Spring 版本 | 破坏 BOM 约束 | NoSuchMethodError、行为漂移 |
| 把所有 Boot 3 能力当成 3.0 就有 | 能力来自不同 Framework/Boot 小版本 | 示例无法编译或配置无效 |
| 把虚拟线程说成 Java 17 | 虚拟线程在 Java 21 正式提供 | 技术选型和容量评估错误 |
| 迁移只测启动 | 请求路径才会触发懒加载组件和业务语义 | 上线后才暴露 Filter、校验或权限问题 |
| 在一个发布中改框架、数据库和业务规则 | 故障无法归因 | 回滚困难、风险成倍增加 |
| 新旧服务共用破坏性数据库变更 | 老版本无法读新结构 | 灰度和回滚失败 |
十五、面试标准回答
知识点页负责解释原理,面试页负责组织标准回答和追问,避免读者只背结论。请进入独立的 Java 17 与 Spring Boot 3.x 面试题。其中每道题都能跳回本页或专题页的精确原理锚点。
十六、关联知识点
- Spring Boot 总览:先建立启动、自动配置和请求链整体认知。
- 启动流程全过程:理解 Environment、ApplicationContext、
refresh()和 WebServer 的阶段。 - 自动配置原理:理解候选类、条件链和 BeanDefinition。
- 配置体系全过程:理解配置源、优先级、Binder 和生产排查。
- Spring Boot 扩展点:迁移公共 Starter 和生命周期扩展。
- Spring Cloud 技术演进:选择 Boot 3 对应的微服务组件。
- Spring Cloud Alibaba:理解三层 BOM 和组件调用链。
- Spring Boot 面试题:只看标准回答和追问,再跳回本页原理锚点。
本章小结
Java 17+ 与 Spring Boot 3.x 不是把版本数字改大。它是一条从 JVM 字节码基线、Spring Framework 6、Jakarta 类型体系、Servlet 容器、自动配置、Security、Observability、AOT,一直延伸到 Spring Cloud 和第三方生态的系统升级链。
真正掌握它,要能同时维护两种现实:理解 JDK 8 + Boot 2.7 存量系统为何仍能工作,也能为新项目正确选择 Java 17/21 和匹配的 Boot 3.x;遇到失败时能从 class 版本、包名、依赖树、自动配置报告、请求契约和生产指标中取得证据,而不是靠“多换几个版本试试”。
