2024 年中我们把一个跑了 5 年的 Spring Boot 2.7 项目升级到 Spring Boot 3.2,踩坑两个月。表面看是 Java 8→17 + javax.*→jakarta.* 的事,实际涉及二三十个依赖兼容性、AOP 切面失效、序列化变化、安全配置重写等。本文实录全过程,给同样要升级的团队避雷。
为什么要升级
Spring Boot 2.7 EOL 时间:2023 年 11 月停免费维护
Spring Boot 3.0 起最低要求:Java 17
Spring Boot 3.x 核心变化:
- javax.* → jakarta.*(命名空间全改)
- 移除大量 deprecated API
- Spring Security 6 重写配置
- Hibernate 6(默认),HQL 语法收紧
- 默认支持 GraalVM 原生镜像
- Observability 标准化(Micrometer Tracing)
业务背景:
- 30 万行代码,40+ 个 microservice 模块
- 主要依赖:MyBatis, Spring Cloud, Redis, RocketMQ
- 团队 25 人,允许 2 个月迁移窗口
升级路径
不要直接 2.7 → 3.x,先做这些准备:
阶段 1(本地修):
- JDK 8 → JDK 17(先用 Java 17 跑 SB 2.7,改完 deprecated 警告)
- 用 OpenRewrite 跑 spring-boot-2.7 → 3 自动迁移
- 跑测试,看通过率
阶段 2(灰度发):
- 单个无状态服务先升,跑一周
- 接入层、关键链路靠后
- 监控观察 GC、内存、错误率
阶段 3(全量):
- 月度发布周期,每周 2-3 个服务升
- 共有库(BOM、starter)升级要先发
坑 1:javax.* → jakarta.*
// Spring Boot 2.x
import javax.servlet.http.HttpServletRequest;
import javax.persistence.Entity;
import javax.persistence.Column;
import javax.validation.constraints.NotNull;
import javax.annotation.PostConstruct;
// Spring Boot 3.x:全改 jakarta
import jakarta.servlet.http.HttpServletRequest;
import jakarta.persistence.Entity;
import jakarta.persistence.Column;
import jakarta.validation.constraints.NotNull;
import jakarta.annotation.PostConstruct;
// IDEA 自动改没问题
// 但有些三方库还卡在 javax,要看更新
# 批量替换工具:OpenRewrite
$ mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2 \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE
# 跑完会自动改:
# - import 路径
# - pom.xml 版本
# - application.yml 配置项重命名
# - 部分 deprecated API 替换
坑 2:Spring Security 6 配置重写
// Spring Boot 2.x 写法(继承 WebSecurityConfigurerAdapter)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().disable()
.httpBasic().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
}
// Spring Boot 3.x 写法(WebSecurityConfigurerAdapter 已删除)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
AuthenticationManager authManager(AuthenticationConfiguration cfg) throws Exception {
return cfg.getAuthenticationManager();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
注意:antMatchers 全部改成 requestMatchers。AntPathRequestMatcher 在 6.x 被 MvcRequestMatcher 替代,路径匹配规则有细微差异(尾部斜杠等)。
坑 3:Hibernate 6 HQL 语法收紧
// Hibernate 5(SB 2.x)能跑
@Query("SELECT u FROM User u WHERE u.email LIKE '%' || :keyword || '%'")
List search(String keyword);
// Hibernate 6(SB 3.x)报错:HQL 不再支持 || 字符串拼接
// 改成 CONCAT 函数
@Query("SELECT u FROM User u WHERE u.email LIKE CONCAT('%', :keyword, '%')")
List search(String keyword);
// 另一个坑:Hibernate 6 默认的命名策略变了
// 表名 / 列名生成方式从 ImplicitNamingStrategyJpaCompliantImpl 变成 CamelCaseToUnderscoresNamingStrategy
// 老项目需要在 application.yml 手动指定:
spring:
jpa:
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
坑 4:Jackson 反序列化 LocalDateTime
// 升级前能跑:Long 时间戳自动反序列化为 LocalDateTime
{"createdAt": 1715000000000}
// 升级后报错:
// JSON parse error: Cannot deserialize value of type `java.time.LocalDateTime`
// from String "1715000000000"
// 修复:全局配置 + 自定义反序列化
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> {
JavaTimeModule mod = new JavaTimeModule();
mod.addDeserializer(LocalDateTime.class, new JsonDeserializer<>() {
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
JsonToken t = p.currentToken();
if (t == JsonToken.VALUE_NUMBER_INT) {
return LocalDateTime.ofInstant(
Instant.ofEpochMilli(p.getLongValue()), ZoneId.systemDefault());
}
return LocalDateTime.parse(p.getText());
}
});
builder.modules(mod)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
};
}
}
坑 5:AOP 切面失效
// 一个老切面,SB 2.x 没问题
@Aspect
@Component
public class LogAspect {
@Around("execution(* com.acme.service.*.*(..))")
public Object log(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
log.info("method={} ms={}", pjp.getSignature(), System.currentTimeMillis() - start);
return result;
}
}
// 升 SB 3 后,有些方法没切到
// 原因:Spring Boot 3 默认 spring.aop.proxy-target-class=true 没变,
// 但部分 service 因为加了 @Configuration / final 类导致 CGLIB 代理失败
// 排查:
// 1. 看启动日志有没有 "Bypassing XAware interfaces" 警告
// 2. final class 不能被 CGLIB 代理 — 把 final 去掉
// 3. 配置类(@Configuration)被 AOP 切的话 proxyBeanMethods 要设 false
// 还有一个坑:Spring 6 对 @AspectJ 表达式更严格
// "execution(* com.acme.service.*.*(..))" 之前能匹配抽象方法的代理
// 6.x 不行,要改成 within(@org.springframework.stereotype.Service *)
坑 6:Feign / OpenFeign 兼容
2023.0.1
3.2.5
// Feign 客户端的 ErrorDecoder 接口变化
@Component
public class CustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
if (response.status() == 404) return new NotFoundException(methodKey);
if (response.status() == 503) return new RetryableException(
503, "Service Unavailable", response.request().httpMethod(), null,
5000L, response.request()); // Long 类型,SB 3 改了签名
return new RuntimeException("Generic error");
}
}
坑 7:RestTemplate / WebClient
// RestTemplate 还在,但 Spring 官方说"基本进入维护模式"
// 推荐用 RestClient(SB 3.2 新加)或 WebClient
// 新写法:RestClient(同步)
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader("Accept", "application/json")
.build();
}
// 使用
ResponseEntity resp = restClient.get()
.uri("/users/{id}", 42)
.retrieve()
.toEntity(User.class);
// 异步还是 WebClient
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("https://api.example.com")
.build();
}
Mono user = webClient.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(User.class);
坑 8:Actuator 端点变化
# SB 2.x:metrics 走 /actuator/prometheus
# SB 3.x:Observability 重构,默认 trace 走 Micrometer Tracing
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
endpoint:
health:
show-details: when-authorized
metrics:
distribution:
percentiles-histogram:
http.server.requests: true
tracing:
sampling:
probability: 1.0
zipkin:
tracing:
endpoint: http://zipkin:9411/api/v2/spans
# 依赖
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
编译测试矩阵
升级后实际收益
指标 SB 2.7 SB 3.2 变化
====================================================
应用启动时间 22s 14s -36%
JVM 内存(空载) 580MB 410MB -29%
GC 频率(Young) 15/min 8/min -47%
Java 编译警告数 400+ 20 -95%
依赖漏洞数(CVE) 18 3 -83%
可选 GraalVM 原生镜像:
- 启动 100ms 内
- 内存 90MB
- 但反射 / 动态代理需要 hints,改造大
避坑清单
- 升 SB 3 前先升 JDK 17,在 SB 2.7 上跑稳了再升
- 用 OpenRewrite 跑自动迁移,但要 review 改动
- Spring Cloud 必须升到 2022+ 版本,老的会编译失败
- Spring Security 6 配置完全重写,不是替换式升级
- Hibernate 6 命名策略变了,老项目显式配置兼容策略
- 三方库要全部 review:Lombok / MapStruct / Logback 都有版本要求
- Mock 框架(Mockito 5)要求 JDK 11+,老的 PowerMock 不兼容
- JSP 项目麻烦,Tomcat 10(jakarta)和 JSP 兼容性差
- 分模块逐步升,单个 microservice 跑通了再推下一个
- 压测对比 GC、内存、QPS,有指标差异要查根因
总结
两个月升级 30 万行代码 40 个服务,实际工时大概用了 1 人月的开发量(自动化迁移 + 修编译错误 + 跑测试)。最难的不是改代码,是搞清楚哪些三方依赖卡住升级。我们遇到过一个内部库还在用 javax.persistence,推动业主团队升级花了三周。升级 SB 3 不只是技术决策,本质是清理三方依赖的契机。如果团队还在 SB 2.x,建议尽早规划,EOL 之后再升只会更难。Java 8 上活太久的项目,SB 3 + JDK 17 + GraalVM 是一次性把技术债打包还清的机会。
—— 别看了 · 2026