如果你最近也在刷开发者社区,大概率看到过“Tibo 发文引热议”的消息。关于那篇文章里具体争论什么,这里不展开聊,但背后有一个问题很值得认真思考:一个项目到底什么时候才算“是时候了”进行技术升级?这两年问得最多的就是 Spring Boot 2.7 要不要升到 Spring Boot 3,升了会不会踩坑,不升又会不会被依赖和安全问题拖住。与其一直停留在“要不要升”的纠结里,不如把整套迁移思路完整拆开,从核心概念、环境准备、代码改动到排错清单,一次性理清楚。
本文会以一个常见的 Spring Boot 2.7 项目为例,按照真实迁移顺序演示如何升级到 Spring Boot 3.2,并覆盖 Java 17、Jakarta EE、Spring Security 6、配置属性变化、第三方依赖兼容性等关键点。无论你是后端开发、项目负责人,还是准备在简历里加上“Spring Boot 3 迁移经验”的学习者,这篇文章都能给你一套可直接落地的参考方案。
1. 背景与核心概念
1.1 Spring Boot 3.0 解决了什么问题
Spring Boot 3.0 是一个跨度非常大的版本,底层基于 Spring Framework 6.0,最低要求 JDK 17。它带来了一大波新能力:
- 全面拥抱 Jakarta EE,将原有的
javax.*命名空间升级为jakarta.*。 - 支持 Native Image,可以结合 GraalVM 将应用编译成原生可执行文件,启动速度和内存占用都有明显优化。
- 更好的可观测性支持,引入 Micrometer Tracing。
- 基础依赖同步升级,比如 Tomcat 10、Hibernate 6、Spring Security 6。
很多团队一直停留在 Spring Boot 2.3、2.5 或 2.7,主要是因为升级风险大、涉及面广。但 Spring Boot 2.7 已经是 2.x 的最后一个功能分支,社区支持和维护会逐渐收紧。对于长期维护的项目来说,“是时候了”并不是头脑发热,而是因为老版本会慢慢变成安全短板和兼容性瓶颈。
1.2 升级 Spring Boot 3 的核心收益
从实际项目角度看,升级带来的收益主要有三类:
| 收益类别 | 说明 |
|---|---|
| 安全与维护 | Spring Boot 2.x 停止 OSS 支持后,漏洞修复和版本更新频率会下降,升级是降低风险的手段 |
| 性能与体验 | 新版本启动速度更快,对容器化部署更友好,内存占用有优化空间 |
| 新特性使用 | 想用虚拟线程、Native Image、更灵活的可观测性,必须以 Spring Boot 3 为基础 |
不过收益不是白来的。升级意味着构建文件、代码包名、安全配置、第三方依赖都需要调整。如果项目里使用了老旧的 MyBatis、ShardingSphere、Druid 等组件,还需要逐一确认兼容版本。
1.3 升级前需要想清楚的问题
在开始动手之前,建议先问自己四个问题:
- 线上系统有多少个微服务?全部升级还是一部分先试点?
- 项目中有没有直接依赖
javax.*的老代码或自定义 starter? - 第三方中间件有没有 Jakarta 兼容版本?
- 核心业务有没有完善的自动化测试可以兜底?
这四个问题决定了升级策略是“激进式全量升级”还是“灰度式分批升级”。大多数情况下,更推荐后者。
2. 环境准备与版本说明
2.1 版本要求
在开始升级之前,先确认本机环境是否满足 Spring Boot 3 的基本要求:
| 组件 | 要求 | 说明 |
|---|---|---|
| JDK | 17 或更高 | 推荐 17 LTS,也可以使用 21 LTS |
| Maven | 3.6.3+ | 需要支持新版插件 |
| Gradle | 7.5+ | 如果使用 Gradle,版本不能太低 |
| 目标 Spring Boot | 3.x | 本文示例以 3.2.x 为例 |
需要注意的是,不同 Spring Boot 3.x 小版本对 JDK 的支持范围有差异。请以官方文档对应版本为准。实际项目中不要盲目追最新,优先选择已经发布一段时间、社区反馈比较稳定的版本。
2.2 示例项目结构
为了演示方便,我准备了一个简单的用户管理项目,技术栈为:
- Spring Boot 2.7.18
- Spring Web
- Spring Data JPA
- Spring Security
- H2 内存数据库
项目结构如下:
upgrade-demo ├── pom.xml └── src ├── main │ ├── java │ │ └── com │ │ └── example │ │ └── demo │ │ ├── DemoApplication.java │ │ ├── config │ │ │ └── SecurityConfig.java │ │ ├── controller │ │ │ └── UserController.java │ │ ├── entity │ │ │ └── User.java │ │ ├── repository │ │ │ └── UserRepository.java │ │ └── service │ │ └── UserService.java │ └── resources │ └── application.yml └── test └── java └── com └── example └── demo └── DemoApplicationTests.java生产环境中的数据源、Redis、消息队列等配置会比这里复杂,但迁移思路是通用的。
2.3 升级前的备份与分支管理
升级属于高风险变更,千万不要直接在主干分支上随意修改。建议从当前主干拉出一个独立分支:
git checkout -b feature/springboot3-upgrade同时保留当前可运行版本的标签,方便回退:
git tag release-springboot-2.7.18如果项目通过私有 Nexus 管理依赖,升级前最好确认目标版本和第三方兼容版本已经同步到私服。
3. 核心变化点拆解
3.1 Java 17 与 Jakarta EE:从 javax 到 jakarta
Spring Boot 3 最重要的底层变化,是从 Java EE 切换到了 Jakarta EE 9。最直观的改动就是项目里大量import javax.*变成了import jakarta.*。
常见的包名变化如下:
| 旧包名 | 新包名 |
|---|---|
| javax.persistence.* | jakarta.persistence.* |
| javax.servlet.* | jakarta.servlet.* |
| javax.validation.* | jakarta.validation.* |
| javax.annotation.* | jakarta.annotation.* |
如果项目里用了 Servlet 过滤器、自定义校验注解、JPA 实体,都需要全局搜索替换。建议先用 IDE 的全局搜索确认影响范围:
搜索内容:javax.persistence 搜索范围:所有文件然后再手动替换。不要直接全局无脑替换,有些第三方依赖内部仍可能引用旧包名,需要同步升级依赖版本。
3.2 Spring Security 6:告别 WebSecurityConfigurerAdapter
Spring Security 6 是 Spring Boot 3 里让很多人头疼的一部分。旧版写法:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated(); } }升级后WebSecurityConfigurerAdapter已经被移除。新的写法是基于SecurityFilterChain的组件式配置:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()); return http.build(); } }这里的关键变化:
authorizeRequests()改成了authorizeHttpRequests()。antMatchers()改成了requestMatchers()。- 使用 Lambda DSL 风格配置。
- 如果不使用 CSRF 防护需要显式关闭。
这样的设计更符合 Spring 的推荐做法,也避免了对全局配置类的继承耦合。
3.3 配置属性变化与自动检测
Spring Boot 3 对很多application.properties或application.yml配置项做了梳理。有些属性被重命名,有些被删除,有些只是迁移到了更清晰的命名空间。
比较常见的像:
# 旧写法 management.metrics.export.prometheus.enabled=true # 新版本部分属性发生变化,需要根据版本确认 management.prometheus.metrics.export.enabled=true为了避免手工排查遗漏,Spring Boot 官方提供了一个迁移辅助依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope> </dependency>添加这个依赖后,应用启动时或配置解析时,会打印属性迁移提示,帮助你发现已失效的配置项。升级完成后,记得把这个依赖从pom.xml中移除。
3.4 第三方依赖兼容性
很多项目升级卡住,不是 Spring Boot 本身的问题,而是第三方 starter 没有跟上 Jakarta 命名空间。例如:
- MyBatis 的 starter 需要升级到支持 Spring Boot 3 的版本。
- Druid 连接池需要确认是否支持
jakarta.*。 - ShardingSphere 需要选择兼容 Spring Boot 3 的分支。
- 老版本 Flyway 需要升级到支持 Spring Boot 3 的版本。
这里无法列出固定版本号,因为版本变化太快,不同项目实际使用的版本也不同。最稳妥的方法是去对应开源项目的官方兼容矩阵中确认,优先选择明确标注支持 Spring Boot 3 / Jakarta 的版本。
4. 完整实战案例:从 Spring Boot 2.7 迁移到 3.2
下面开始走一遍完整的迁移过程。
4.1 修改构建文件
首先修改pom.xml中的 Spring Boot 版本:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.1</version> <relativePath/> </parent>同时把 Java 版本调整为 17:
<properties> <java.version>17</java.version> </properties>如果你使用 Gradle,对应修改:
plugins { id 'org.springframework.boot' version '3.2.1' id 'io.spring.dependency-management' version '1.1.4' id 'java' } java { toolchain { languageVersion = JavaLanguageVersion.of(17) } }注意,Spring Boot 3 要求父 POM 或 BOM 的版本不能低于 3.0.0。如果项目里是通过自定义父 POM 引入依赖管理,需要确认spring-boot-dependencies的版本也一起更新。
4.2 修改启动类和实体类
启动类通常不需要大改:
// 文件路径:src/main/java/com/example/demo/DemoApplication.java package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }实体类中需要替换 JPA 依赖的包名:
// 文件路径:src/main/java/com/example/demo/entity/User.java package com.example.demo.entity; import jakarta.persistence.*; @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; public User() { } public User(String name) { this.name = name; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }如果你的项目里还使用了javax.validation.constraints.*,也要统一替换为jakarta.validation.constraints.*。
4.3 编写 Controller、Service、Repository
Controller、Service、Repository 这三层在大多数情况下可以保持原有代码结构。
UserController:
// 文件路径:src/main/java/com/example/demo/controller/UserController.java package com.example.demo.controller; import com.example.demo.entity.User; import com.example.demo.service.UserService; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping public List<User> list() { return userService.list(); } @PostMapping public User create(@RequestBody User user) { return userService.save(user); } }UserService:
// 文件路径:src/main/java/com/example/demo/service/UserService.java package com.example.demo.service; import com.example.demo.entity.User; import com.example.demo.repository.UserRepository; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserService { private final UserRepository userRepository; public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public List<User> list() { return userRepository.findAll(); } public User save(User user) { return userRepository.save(user); } }UserRepository:
// 文件路径:src/main/java/com/example/demo/repository/UserRepository.java package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; public interface UserRepository extends JpaRepository<User, Long> { }从代码层面可以看到,升级的核心并不在业务代码,而在于依赖版本和框架 API 的变化。
4.4 重写 Spring Security 配置
把原来继承WebSecurityConfigurerAdapter的配置类删掉,替换为基于SecurityFilterChain的写法:
// 文件路径:src/main/java/com/example/demo/config/SecurityConfig.java package com.example.demo.config; 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.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .requestMatchers("/api/users/**").authenticated() .anyRequest().permitAll()) .httpBasic(Customizer.withDefaults()); return http.build(); } }这里用requestMatchers替代了antMatchers,同时把 URL 匹配规则集中在一个authorizeHttpRequests中,整体阅读起来更清晰。
4.5 修改 application.yml
接下来调整配置文件。示例配置如下:
# 文件路径:src/main/resources/application.yml server: port: 8080 servlet: context-path: /demo spring: application: name: upgrade-demo datasource: url: jdbc:h2:mem:testdb driver-class-name: org.h2.Driver username: sa password: jpa: hibernate: ddl-auto: create-drop show-sql: true management: endpoints: web: exposure: include: health,info,metrics如果项目原先配置了spring.datasource.driver-class-name为com.mysql.jdbc.Driver,升级时注意驱动类名可能已经变化。比如 MySQL 官方新版驱动推荐使用:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver如果你的项目是纯 H2 示例,则不需要额外处理。
4.6 处理第三方依赖
在示例项目中,假设没有复杂第三方依赖,因此升级后依赖比较简单。实际项目中,第三方依赖的处理是最容易出现意外的地方。
比如老版本的 MyBatis 启动器:
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency>这类旧版本可能无法直接兼容 Spring Boot 3。需要去 MyBatis 官方社区查看支持 Spring Boot 3 的 starter 版本,升级后再重新编译。
4.7 运行与验证
完成上述修改后,可以先执行编译:
mvn clean compile如果编译通过,再执行测试:
mvn test最后运行应用:
mvn spring-boot:run启动成功后,访问接口:
- 查询用户列表:
GET http://localhost:8080/demo/api/users - 健康检查:
GET http://localhost:8080/demo/actuator/health
由于接口上有安全配置,未带认证信息访问/api/users会返回 401。因为 H2 内存数据库会在应用启动时初始化表结构,所以可以先调用POST接口新增用户,再调用GET接口查看数据。
如果你看到类似下面的输出,说明应用已经正常启动:
Tomcat started on port 8080 (http) with context path '/demo' Started DemoApplication in 2.5 seconds4.8 使用迁移辅助工具
除了手动修改,Spring 官方还提供了一些迁移辅助工具,例如 Spring Boot Migrator。这类工具可以扫描老项目,并尝试自动完成部分迁移。不过自动化工具处理不了所有场景,尤其是复杂的安全配置和第三方依赖。建议把工具当作辅助,最终还是要靠人工 review 和测试兜底。
5. 常见问题与排查思路
升级过程中最容易遇到下面这些报错,这里整理成一份排错清单。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
ClassNotFoundException: javax.servlet.Filter | 依赖引用了旧javax包 | 全局搜索javax.servlet,改为jakarta.servlet,并升级相关依赖 |
NoClassDefFoundError: WebSecurityConfigurerAdapter | Spring Security 6 已移除旧适配器 | 改为使用SecurityFilterChainBean |
应用启动失败,提示PathPattern匹配问题 | Spring Boot 3 默认使用PathPatternParser | 检查 Controller 和 SecurityConfig 中 URL 通配符,推荐使用/** |
| 数据库方言错误 | Hibernate 6 方言配置变化 | 删除手动配置的 dialect,让 Hibernate 自动识别 |
| MyBatis 相关 Bean 无法注入 | MyBatis starter 版本太旧 | 升级到支持 Spring Boot 3 的版本 |
配置属性提示Unknown property | Spring Boot 3 清理了老属性 | 添加spring-boot-properties-migrator定位,按提示迁移 |
java.lang.reflect.InaccessibleObjectException | JDK 17 模块限制 | 优先升级依赖,不要一上来就加--add-opens |
| 启动耗时变长或部分 Bean 加载失败 | 依赖注入或自动配置类被误替换 | 查看启动日志中的BeanCreationException堆栈,定位到具体配置 |
在实际项目里,最常见的情况不是某个大坑,而是各种小问题叠加在一起。所以升级过程中要保持耐心,建议每改完一部分就编译一次,不要等到最后一起解决所有报错。
6. 最佳实践与工程建议
6.1 升级前先做依赖清单盘点
花半天时间把项目里所有依赖梳理出来,重点标记三类:
- 官方维护的 Spring Boot starter。
- 第三方 starter 和 SDK。
- 项目内部公共模块。
这样升级时就能快速判断哪些依赖需要同步升级,哪些依赖可能没有兼容版本。
6.2 使用灰度发布,不要一把梭
线上系统升级不建议一次全部替换。可以把 1 到 2 个非核心服务先升级,观察运行稳定性和性能指标。确认没问题后,再逐步扩大范围。
如果服务通过 Nacos、Consul 等服务注册中心管理,可以结合流量权重做灰度。例如先切 10% 流量到新版本,观察错误率、耗时、GC 情况。
6.3 测试要提前准备
升级前先确认测试覆盖率。重点测试:
- 登录认证和权限控制链路。
- 用户核心读写链路。
- 定时任务和消息消费链路。
- 对外提供的 OpenAPI 接口。
测试不一定要多,但核心链路必须能自动化回归。否则升级后出现问题,很难判断是代码问题还是配置问题。
6.4 理解并利用迁移辅助依赖
在升级过程中,可以在pom.xml临时加入:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-properties-migrator</artifactId> <scope>runtime</scope> </dependency>启动时它会提示哪些属性已经变化或是无效。定位并修复所有提示后,移除这个依赖,避免把调试信息带到生产环境。
6.5 关注安全与权限变更
Spring Security 6 的默认行为比旧版本更严格。尤其在 CSRF、跨域配置、请求匹配规则上,不要为了通过测试而随意关闭安全限制。生产环境使用最小权限原则,对外暴露的接口尽量显式配置白名单,不要使用anyRequest().permitAll()覆盖所有路径。
6.6 保留回滚方案
升级部署前,除了常规备份,还需要保留旧版本的可运行产物。建议在发布流程中加上回滚按钮或快速回退脚本。回滚不仅依赖代码,还依赖数据库变更。如果升级过程中有增量 SQL 脚本,要评估向下兼容性,避免回滚后数据库结构和旧代码不匹配。
7. 总结与学习路线
Spring Boot 3 迁移并不可怕,真正需要重视的是变化点梳理和回归验证。本文通过一个完整的示例项目,演示了从 Spring Boot 2.7 升级到 3.2 的核心步骤,覆盖了 Java 17、Jakarta EE、Spring Security 6、配置属性迁移和第三方依赖兼容性等关键内容。
完成基础迁移后,下一步可以继续学习:
- Spring Security 6 的授权语义和过滤器链机制。
- Spring Boot 3 对 GraalVM Native Image 的支持。
- JDK 21 虚拟线程在 Spring Boot 3.2 中的实践。
- Micrometer Tracing 与可观测性体系搭建。
对于实际项目,建议优先关注安全配置、中间件兼容性和回滚预案,不要只盯着版本号。升级本身不是目标,让项目更稳定、更可持续演进才是“是时候了”的真正含义。
如果这篇文章对你有帮助,可以先收藏备用。动手迁移时如果遇到新的问题,欢迎在评论区一起讨论。