Spring Boot 3 升级两个月实录:javax→jakarta + 30 万行代码 + 40 个微服务

5 年老项目 SB 2.7 升 3.2,踩 8 大坑:javax→jakarta、Security 6 重写、Hibernate 6 HQL、Jackson LocalDateTime、AOP 失效、Feign 不兼容、RestClient、Actuator Observability。OpenRewrite 自动化 + 30 万行代码 + 40 微服务实战。

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,改造大

避坑清单

  1. 升 SB 3 前先升 JDK 17,在 SB 2.7 上跑稳了再升
  2. 用 OpenRewrite 跑自动迁移,但要 review 改动
  3. Spring Cloud 必须升到 2022+ 版本,老的会编译失败
  4. Spring Security 6 配置完全重写,不是替换式升级
  5. Hibernate 6 命名策略变了,老项目显式配置兼容策略
  6. 三方库要全部 review:Lombok / MapStruct / Logback 都有版本要求
  7. Mock 框架(Mockito 5)要求 JDK 11+,老的 PowerMock 不兼容
  8. JSP 项目麻烦,Tomcat 10(jakarta)和 JSP 兼容性差
  9. 分模块逐步升,单个 microservice 跑通了再推下一个
  10. 压测对比 GC、内存、QPS,有指标差异要查根因

总结

两个月升级 30 万行代码 40 个服务,实际工时大概用了 1 人月的开发量(自动化迁移 + 修编译错误 + 跑测试)。最难的不是改代码,是搞清楚哪些三方依赖卡住升级。我们遇到过一个内部库还在用 javax.persistence,推动业主团队升级花了三周。升级 SB 3 不只是技术决策,本质是清理三方依赖的契机。如果团队还在 SB 2.x,建议尽早规划,EOL 之后再升只会更难。Java 8 上活太久的项目,SB 3 + JDK 17 + GraalVM 是一次性把技术债打包还清的机会。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

API 网关选型三个月对比:Kong / APISIX / SCG / Envoy / Higress

2026-5-19 12:00:38

技术教程

Node.js event loop 阻塞排查实录:p99 从 8s 降到 250ms

2026-5-19 12:04:41

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索