前言
最初的目标很简单:将项目从 Spring Boot 3.2.3 直接升级到 4.0.0(当前最新 GA)。
实际操作后发现,这种“一步登天”的做法并不现实:编译错误、弃用 API、大量隐式行为变更接踵而至,在对项目配置和底层机制理解不够深入的前提下,几乎无法推进。
在查阅官方资料后,我注意到 Spring Boot 官方 Wiki 已明确给出建议:
升级到 4.0.0 之前,务必先升级到 3.5.x
这并不是形式建议,而是对真实迁移成本的精准判断。
因此,我最终选择 分阶段升级,在保证系统可运行的前提下,逐步体会 Spring Boot 的演进方向与设计变化。
重要前置条件
如果目标版本是 Spring Boot 4.0.0,必须提前将 Java 版本从 17 升级到 21,否则项目将无法通过编译。
一、3.2.3 → 3.3.0:版本兼容的第一道门槛
升级 spring-boot.version 后,项目启动即失败。
问题定位
报错信息指向 Spring Boot 与 Spring Cloud 版本不兼容。
这一点在 Spring Cloud 官方文档 中有明确说明:
- Spring Boot 3.3.x
- Spring Cloud 2023.0.x
我当前使用的是 2023.0.0,但根据 Spring Cloud 2023.0 Release Notes,该版本并非最终推荐版本。
尝试升级到 2023.0.6 后,项目恢复正常运行。
<spring-cloud.version>2023.0.6</spring-cloud.version>说明
后续每一次 Spring Boot 升级,都需要同步升级 Spring Cloud。
最终在 4.0.0 阶段,对应的 Spring Cloud 版本为 2025.1.0。
二、3.3.0 → 3.4.0:注解迁移与测试的价值
升级后出现编译错误:
import org.jetbrains.annotations.NotNull;问题本质
jetbrains 注解不再被 Spring 官方生态推荐使用。
此时有一个实用技巧:
先移除错误依赖,让 IDE 自动提示可替代的注解来源
IDE 给出了多个选项,其中主要是:
jakarta.validation.constraints.NotNullcom.sun.istack.NotNull
选择依据
jakarta.validation.constraints.NotNull- Java 标准校验注解
- 用于参数、字段的非空约束
- Spring 生态主流方案
com.sun.istack.NotNull- 多用于 JAXB / XML 序列化
- 不适用于 Web 层参数校验
结论:选择第一个。
本阶段的最大感悟:测试的价值被低估了
升级过程中,并没有修改任何核心业务逻辑;
项目能启动,几个关键接口也能跑通——但心里并不踏实。
这正是单元测试与集成测试真正发挥价值的时刻:
- 正所谓
失之东隅,收之桑榆 - 它不是为了写得“好看”
- 而是为了在非功能性改动(如框架升级)中,验证核心逻辑仍然成立
事实证明,已有的数十个测试用例,让这次升级可控、可验证、可回滚。
三、3.4.0 → 3.5.0:平稳过渡
这一阶段没有遇到明显问题,项目可直接运行。
这恰恰印证了一个事实:
当你遵循官方升级路径时,Spring 的演进是克制且连续的。
升级和编程学习之路是类似的,一步一个脚印,明确每个阶段的目标,且钻研进去,切莫好高骛远。
四、3.5.0 → 4.0.0:真正的迁移开始
从这一阶段开始,问题全部集中在编译期,但每一个都指向了明确的设计变更。
问题一:Jackson 配置方式变更(核心难点)
旧代码中使用了:
Jackson2ObjectMapperBuilderCustomizer该接口在新版本中被弃用。
原配置的意图
确保 未显式声明 @JsonView 的字段仍然参与序列化。
这是一个非常实用的策略,尤其在需要精细控制返回字段时。
新方案
Spring Boot 4.x 推荐使用:
JsonMapperBuilderCustomizer等价实现如下:
@Bean
public JsonMapperBuilderCustomizer jsonCustomizer() {
return builder -> builder.enable(MapperFeature.DEFAULT_VIEW_INCLUSION);
}问题二:Feign 解码器 API 变更
原有代码引用:
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
HttpMessageConverters报错,最开始以为是依赖变更的问题,后来发现当前配置feign文件的下面代码报错。这里的解决,完全参考下面idea的提示:
修复后构造方式如下:
public class SpringDecoder implements Decoder {
private final ObjectProvider<FeignHttpMessageConverters> converters;
public SpringDecoder(ObjectProvider<FeignHttpMessageConverters> converters) {
this.converters = converters;
}
...
}问题三:Specification 查询的空条件歧义
Spring Data JPA 在新版本中不再容忍 null 作为 where 条件。
这里的问题是:新版本查询,当where条件为null时,spring不知道用的是哪一个类型,看了源码好久,最后发现可以直接使用unrestricted()来代替。
解决方案是显式返回:
return Specification.unrestricted();该方法语义清晰,避免类型歧义。
问题四:PathMatcher 被彻底弃用
旧配置目标:
- 使用字符串规则匹配 URL
- 忽略大小写
新版本中 PathMatcher 被移除,推荐使用 PathPatternParser:
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
PathPatternParser parser = new PathPatternParser();
parser.setCaseSensitive(false);
configurer.setPatternParser(parser);
}这是一次从运行时字符串匹配 → 预编译路径模式的设计转变。
问题五:DaoAuthenticationProvider 构造器变更
原无参构造器被移除,新版本必须显式传入:UserDetailsService。
这是一次安全配置显式化的调整,修改方式直接、明确。修复的方式很简单,这里不给出修复代码,防止篇幅过长。
问题六:无法再重写 matches 方法(OTP 场景)
旧方案依赖重写 PasswordEncoder#matches,新版本已禁止该用法。
最终决策
放弃技巧性 override,改为自定义 AuthenticationProvider。
具体逻辑如下:
@Component
public class CustomizedDaoAuthenticationProvider extends DaoAuthenticationProvider {
private final OneTimePasswordService oneTimePasswordService;
public CustomizedDaoAuthenticationProvider(PasswordEncoder passwordEncoder,
OneTimePasswordService oneTimePasswordService,
UserDetailsService userDetailsService) {
super(userDetailsService);
this.setPasswordEncoder(passwordEncoder);
this.oneTimePasswordService = oneTimePasswordService;
}
@Override
protected void additionalAuthenticationChecks(@NonNull UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) {
String password = null;
if (authentication.getCredentials() != null) {
password = authentication.getCredentials().toString();
}
// 1. 先尝试一次性密码
if (oneTimePasswordService.matches(password)) {
return; // 验证通过
}
// 2. 正常密码验证
if (!getPasswordEncoder().matches(password, userDetails.getPassword())) {
throw new BadCredentialsException("密码错误");
}
}
}这不仅符合新版本约束,也让认证逻辑更加清晰。
问题七(不会影响运行):Redis 序列化弃用提示
参考源码推荐替换为:
GenericJacksonJsonRedisSerializer该调整涉及 多态反序列化,属于架构层面问题,后续单独展开。
问题八:测试依赖的模块化重构与注解迁移
在解决上述运行时问题后,项目终于能够正常启动。但当我执行 mvn test 运行本地测试时,却遇到了多个与测试配置相关的编译和运行错误。这让我意识到,Spring Boot 4.0 在测试支持上的变化是最彻底、最需要细致处理的迁移点之一。
问题本质:测试依赖的模块化设计
Spring Boot 4.0 引入了全面模块化(modularization)架构,这直接影响了测试 classpath。旧版本中,我们通常只需添加一个“万能”的测试 starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>它会隐式拉取几乎所有测试支持(MockMvc、Security 测试注解、JPA 测试切片等),非常方便,但也导致 classpath 臃肿,且依赖不明确。
新版本中,这个万能 starter 被拆分成细颗粒度的技术特定 test starter,每个对应一种主流技术栈。官方迁移指南明确指出:
“Given that all test 'starter' POMs bring spring-boot-starter-test transitively, you don’t need to define this starter anymore. Rather, you should list the starters of the technologies that are under test for the module.”
这意味着:
- 不再直接依赖
spring-boot-starter-test(它现在仅提供核心测试库,如 JUnit 6、Mockito 5、AssertJ)。 - 必须显式添加项目实际使用的 test starter,否则某些 auto-configuration(如 MockMvc、
@WithMockUser)将不会自动启用,导致测试失败。
在本项目中,使用了 WebMVC、Security、Flyway 等技术,因此需要添加对应的 test starter:
<dependencies>
<!-- 测试依赖:按需添加模块化 test starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webmvc-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-flyway-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 如果还有其他技术,如 data-jpa-test、data-redis-test 等,也需类似添加 -->
</dependencies>迁移技巧与过渡方案
- 逐步迁移:如果项目测试众多,一次性切换所有 starter 可能引入大量错误。可以先临时引入
spring-boot-starter-test-classic(test 作用域),它模拟旧版行为,提供所有测试模块,便于快速验证应用运行。随后逐步替换为具体 test starter,并移除 classic。 - Maven 版本兼容:顺带确认
mvn -v显示的 Maven 支持 Java 21(Spring Boot 4.0 要求),否则编译测试也会失败。
另一个关键变化:MockBean 的弃用
除了依赖,另一个常见报错来自测试注解:
Spring Boot’s @MockBean and @SpyBean support has been deprecated in this release, in favor of @MockitoBean and @MockitoSpyBean support.
旧版广泛使用的 @MockBean(用于在 Spring 上下文中注入 Mockito mock)已被弃用,取而代之的是 Spring Framework 原生的 @MockitoBean 和 @MockitoSpyBean。
迁移方式简单直接:
- 将测试类或字段上的
@MockBean替换为@MockitoBean。 @SpyBean同理替换为@MockitoSpyBean。- 如果需要共享 mock 配置,可将
@MockitoBean用作元注解创建自定义组合注解。
这一变化背后的动机是减少 Spring Boot 对 Mockito 的自定义包装,转向更标准、更轻量的 Framework 支持。同时,新注解在某些场景(如 @Configuration 类中)行为略有差异,但对于大多数测试类字段使用是直接替换。
本阶段感悟
测试部分的迁移看似“琐碎”,却最能体现 Spring Boot 4.0 的设计哲学:从隐式到显式,从 monolithic 到 modular。它迫使我们审视项目实际依赖的技术栈,避免不必要的 classpath 膨胀。同时,再次验证了完整测试套件在框架升级中的防护作用——没有它们,许多隐蔽问题会在生产环境才暴露。
如果你的项目测试覆盖率较高,这一阶段会相对顺利;反之,建议借迁移机会补齐测试。总之,按官方指南逐个添加所需 test starter,并全局替换 @MockBean,即可平稳过渡。
总结
这次升级最大的收获并不在于“成功跑起来”,而在于:
- 明确了 Spring 官方推荐的升级节奏
- 理解了多个 API 变更背后的设计动机
- 验证了 测试在非功能性改动中的核心价值
Spring Boot 4.0 并不激进,但它不再纵容模糊与隐式行为。
如果你准备升级,建议和我一样:慢一点,但走稳。
在此,再次万分感谢潘老师给予我的机会。师傅领进门,修行在个人。我会继续努力,稳扎稳打,亦步亦趋。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用。你还可以使用@来通知其他用户。