老炮踩坑录 · U01 · 升级实战系列 · 推演篇
基于「企业融合评估平台」真实源码,做一次 2.1.0 → 3.5 的迁移推演
关键词:javax → jakarta · Springfox · Druid 改名 · OpenRewrite · 外置 Tomcat 10
👋 欢迎阅读
🏠个人主页: 知守观
📘我的专栏: 老炮踩坑录
💻当前内容:Spring Boot 迁移推演
引子
先交代一句:上篇预告写的是"迁移实战:我踩了多少坑",准确说这篇是推演。我没有把这个项目完整升到 3.5.x 跑通,而是基于真实源码,按"如果真升,会在哪一步炸"倒推了一遍。目标线是 3.5.x,4.0 暂不纳入。文中每节标了【源码可证】和【需编译验证】:能跑命令验证的跑了,跑不了的写清楚是推断。真升实跑会在后面的实战篇补上。
2023 年底 Spring 官方宣布 2.x 停止开源支持那天,我在前同事群里回了一句:欠的债,迟早是要还的。
话是这么说,我在职的时候版本一直没有升级。这个项目 2019 年立项,2022 年我走的时候还跑在 2.1.0.RELEASE 上------一个 2018 年 10 月发布的版本。fastjson 1.2.37 的坑我之前专门写过一篇,当时就有读者留言:你的 pom 文件不全换一遍,睡得着吗?
睡不着。离职之后我把这个项目的源码当标本,完整推演了一遍 2.1.0 → 3.5.x 的迁移路径------所有坑都按"如果真升,会在哪一步炸"来倒推,能跑命令验证的我跑了,跑不了的我标注清楚推演依据,不把推测当既成事实。
Spring Boot 4.0 去年底也发了,我没把它纳入推演------大版本头一年的坑留给别人踩吧,我的目标线是 3.5.x。文中每节开头都标了证据级别:【源码可证】表示直接能在仓库里找到对应代码或配置;【需编译验证】表示基于依赖关系和官方迁移指南的推断,真跑一遍才敢拍板。
家底:这个 pom.xml 还停在 2018 年
先看这个病人长什么样(pom.xml 头部,原样):
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<packaging>war</packaging>
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
JDK 8、war 包、2.1.0。再看依赖清单里的几位老朋友:
| 依赖 | 版本 | 出生年份 | 问题 |
|---|---|---|---|
| fastjson | 1.2.37 | 2018 | 漏洞一大堆,F03 写过 |
| springfox-swagger2 | 2.9.2 | 2018 | 项目 2020 年停更,已死 |
| druid-spring-boot-starter | 1.1.13 | 2018 | 不认识 jakarta |
| mybatis-plus-boot-starter | 3.3.0 | 2019 | 拖着一个 mybatis-spring 2.x |
| pagehelper-spring-boot-starter | 1.2.5 | 2018 | 同上 |
| poi | 3.12 | 2015 | 全 pom 最老,11 岁 |
| guava | 20.0 | 2016 | 凑合能用 |
| mysql-connector-java | 随 parent | --- | 连 artifactId 都被官方改了 |
这个 pom 的年龄分布本身就是一部拖更史。2015 到 2019 年的包混在一起,能跑全靠运气和 JDK 8 的宽容。
第一刀砍向哪:推演路径我选"一步到位"
官方文档的建议路线是逐版本爬:2.1 → 2.7 → 3.0 → 3.5。推演时我评估了一下,放弃了。
理由很简单:这个项目没有测试覆盖率兜底,逐版本升意味着每个中间版本都要人工回归一遍,2.7 版本升级那一遍纯属浪费------反正终点是 3.5。我推演时按一步到位来:parent 直接改 3.5.x、JDK 直接上 17,让编译器当清单用。
按照 Spring Boot 3 + JDK 17 的实际迁移经验,第一轮 mvn clean compile 通常是四百多个错误起步。这个数字是社区反复验证过的量级,不是我编的------但具体到这个项目的精确数字,需要真跑才知道。分类之后大致长这样:
text
编译错误 400+(第一轮 mvn clean compile)
├── javax.servlet.* 找不到 占九成 ← OpenRewrite 一键迁移
├── javax.annotation 找不到 6 处 ← JDK 11 把这个包删了
├── springfox API 消失 1 个类 ← 换框架,手工
├── Druid / MyBatis-Plus 版本错配 编译没报,运行时找上门
└── 其余零散 若干
九成错误是同一件事:javax 包没了。真正烧时间的是剩下那一成。下面按推演时的处理顺序讲。
坑 1:JDK 17 先把 javax.annotation 没收了 【源码可证 + 需编译验证】
【源码可证】BackDeclareListServiceImpl.java 第 44 行确实引了 javax.annotation.PostConstruct。JDK 8 时代它直接用 JDK 自带的 java.xml.ws.annotation 模块,JDK 11 把这个模块整个删了,17 自然也没有。Spring Boot 3 里它的新家在 jakarta:
java
// 改前
import javax.annotation.PostConstruct;
// 改后
import jakarta.annotation.PostConstruct;
jakarta.annotation-api 跟着 spring-boot-starter 传递依赖进来,改个 import 就完事。
JDK 8 → 17 本身还带来两个小麻烦。
- 一是 Lombok:老版本在 17 上编译直接抛异常,好在这个项目没写死版本号,跟着新 parent 自动升到了支持 17 的版本------没写死版本号这时候反而是红利。
- 二是 POI 3.12,2015 年的包,在 17 上控制台警告刷屏。具体哪些 API 会在运行时炸我没逐个试,推演结论是直接升 5.2.5,没兴趣拿自己的项目赌。Excel 导出的问题在之前的文章里讲过,这里不再展开。
坑 2:javax → jakarta,全局替换差点坑死我 【源码可证】
它是整个迁移里体力活中的大头。动手前先统计工作量------这个数是我刚才真实跑的:
bash
grep -r "^import javax\." src/main/java | wc -l
# 120 处,分布在 45 个文件里
120 处 import,谁看谁头疼。于是我跟所有人一样,动了全局替换的念头:javax. → jakarta.,一把下去收工。
幸好动手之前多扫了一眼。这个项目里混着两种 javax:
java
// SSL.java ------ 7 个 import 全是 javax.net.ssl
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
java
// AesEncodeUtil.java
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
javax.net.ssl、javax.crypto、javax.naming、javax.sql 这些是 Java SE 的包,归 JDK 管,永远姓 javax,改成 jakarta 直接编译不过。要搬家的只有 Jakarta EE 那一系:javax.servlet、javax.annotation、javax.persistence、javax.validation、javax.mail、javax.inject。
这个项目 120 处里,114 处该改,6 处碰都不能碰。比例虽然悬殊,但全局替换不跟你讲比例。
小项目手工改,大项目最好上 OpenRewrite,唯独别全局 sed。
坑 3:OpenRewrite 干了 80% 的体力活,剩下 20% 才是钱 【需编译验证】
说到 OpenRewrite,pom 里挂个插件就能跑(我这次没真跑,下面是它的标准能力):
xml
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>5.43.0</version>
<configuration>
<activeRecipes>
<recipe>org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2</recipe>
</activeRecipes>
</configuration>
<dependencies>
<dependency>
<groupId>org.openrewrite.recipe</groupId>
<artifactId>rewrite-spring</artifactId>
<version>5.20.0</version>
</dependency>
</dependencies>
</plugin>
版本号和 recipe(迁移规则)名凭记忆写的,这套东西更新很快,以官网当前文档为准。
跑完 mvn rewrite:run 之后,按它的官方能力清单,它能替你干的:
- 114 处 javax → jakarta 重命名,全对。SSL.java 和 AesEncodeUtil.java 一碰没碰------它认得 Java SE 和 Jakarta EE 的边界,这一点比人的眼睛可靠
- 一部分 Spring 配置属性改名
- 几个废弃 API 的替换建议
它干不了的:
- 换框架。 springfox → springdoc 是换库,没有对应 recipe,手工
- 换 artifactId。 Druid、MyBatis-Plus 那批 starter 改名(下面讲),这种"starter 自己分叉"的情况它管不了
- 业务代码里的脏活。 之前文章里那个静态 ThreadLocal,迁移工具看都不看一眼
我的结论:OpenRewrite 把重命名这种体力活自动化了,值得用。迁移的钱主要花在它处理不了的地方。
坑 4:Springfox 死了,Swagger 整条链跟着报废 【源码可证 + 需编译验证】
【源码可证】全 pom 死得最透的依赖。springfox 2020 年之后停更,Boot 2.6 把默认路径匹配策略换成 PathPatternParser 它就半残,到了 3.x 直接报废。
替代品没有悬念,springdoc-openapi。改法(SwaggerConfig.java,改前是仓库里的真实代码,改后是 springdoc 的标准写法):
java
// 改前:springfox 三件套
@Profile({"dev","test"})
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket customDocket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
}
java
// 改后:springdoc
@Profile({"dev","test"})
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI().info(new Info()
.title("两化融合")
.description("两化融合API")
.version("1.1.0"));
}
}
@EnableSwagger2 整个删掉,Docket/ApiInfoBuilder 这套 API 作废,换成 OpenAPI/Info。页面地址从 /swagger-ui.html 变成 /swagger-ui/index.html,文档数据地址从 /v2/api-docs 变成 /v3/api-docs。
这个 URL 变化还炸掉了一个我差点忘了的东西。pom.xml 里躺着一条离线文档生产线:
xml
<swagger_source_url>http://localhost:8080/v2/api-docs</swagger_source_url>
...
<groupId>io.github.swagger2markup</groupId>
<artifactId>swagger2markup-maven-plugin</artifactId>
swagger2markup 吃的是 Swagger 2 格式的 /v2/api-docs,springdoc 吐的是 OpenAPI 3 的 /v3/api-docs,格式对不上,整条链报废。而 swagger2markup 自己也停在 1.3.1,没人维护了。
推演时我的处理建议是:砍了。2022 年之后没人用那堆离线 html 查过接口------这是我离职前的观察,离职后没再问过前同事。迁移的时候你会发现不少这种没人用也没人删的基建------平时没人提,因为没人用;你不碰它,它就一直在 pom 里假装活着。
坑 5:国产 starter 集体改名,编译过了也可能运行时炸【需编译验证】
这一坑最阴。几个关键依赖的新老坐标:
| 老坐标(Boot 2 线) | 新坐标(Boot 3 线) |
|---|---|
| mysql:mysql-connector-java | com.mysql:mysql-connector-j |
| com.alibaba:druid-spring-boot-starter:1.1.13 | com.alibaba:druid-spring-boot-3-starter:1.2.x |
| com.baomidou:mybatis-plus-boot-starter:3.3.0 | com.baomidou:mybatis-plus-spring-boot3-starter:3.5.7 |
| com.github.pagehelper:pagehelper-spring-boot-starter:1.2.5 | 同名,版本升 2.1.0 |
三个坑点逐个说。
- MySQL 驱动连 groupId 都换了。 老的
mysql:mysql-connector-java停在 8.0.33 永久停更,官方新家是com.mysql:mysql-connector-j。这个项目 pom 里没写版本号,靠 parent 管理,换上新坐标后版本自动跟上------这是又一个没写死版本号的红利。 - Druid 的 starter 名字里多了个
-3-。druid-spring-boot-3-starter才是给 Boot 3 用的。老名字那个 starter 在 Maven 仓库里也有看起来很新的版本,但那是 Boot 2 线的,放进来会拖进 javax 时代的传递依赖。老 starter 配 Boot 3 具体会怎么死------编译报错还是数据源悄悄失效------我没实测。推演时的结论是看到官方给了专用 starter 就直接换,没兴趣拿自己的项目做实验。 - MyBatis-Plus 的运行时炸。 3.3.0 拖的是 mybatis-spring 2.x,而 Spring 6 需要 mybatis-spring 3.0+。这种版本错配编译期可能一声不吭,运行时一个
NoSuchMethodError甩你脸上。换成mybatis-plus-spring-boot3-starter3.5.7 之后,PageHelper 也得跟着升 2.x------它和 MyBatis-Plus 的兼容是按版本组合算的,单独升任何一个都可能出问题。这是我翻 GitHub issue 区得到的教训,没亲自验证所有组合,如实交代。
坑 6:war 包和外置 Tomcat------最贵的一颗雷不在代码里 【源码可证】
【源码可证】回到 pom 头部那个 <packaging>war</packaging>。这个项目有两个启动类,其中一个是给外置容器准备的:
java
// SpringBootStartApplication.java ------ 给外置 Tomcat 用
@SpringBootApplication
@EnableScheduling
public class SpringBootStartApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(JSenterpriseFrameApplication.class);
}
}
pom 里 Tomcat 是 provided,war 打出来扔到服务器上的外置 Tomcat 跑。
问题来了:服务器上那个 Tomcat 是 9。Tomcat 9 是 javax 世界,10 才是 jakarta 世界。jakarta 编译出来的 war 往 Tomcat 9 上一扔,类加载直接炸------jakarta.servlet.Servlet 这个类在 9 的类加载器里根本不存在。
也就是说,这次迁移的最后一公里不在我手里,在运维手里:外置 Tomcat 必须换 10.1+。如果那台 Tomcat 上还跑着别的老 war(实际情况往往如此),它升不了,只能给这个项目单开一个 Tomcat 10 实例,或者干脆改回内嵌 Tomcat 打 jar 部署。
这一项在任何迁移 recipe 里都找不到,但给迁移做预算的时候,它经常比代码本身还贵。我在职时见过别的团队有栽在这上面的。
坑 7:那些不报错但行为变了的静默变化 【源码可证 + 需编译验证】
编译过了、跑起来了,还剩几个行为级的变化要查,它们不会用异常提醒你。
- 路径匹配策略。 Spring Boot 2.6 起默认从 AntPathMatcher 换成 PathPatternParser。项目里
AuthHandlerInterceptor注册了一堆拦截 pattern,大多数写法两种解析器兼容,但如果哪里写了带路径片段通配的老 pattern,3.x 下要么启动报错,要么匹配行为悄悄改变。全部 pattern 人工过一遍,别偷懒。 - 配置前缀搬家。
spring.redis.*在 Boot 3 挪到了spring.data.redis.*。配置文件里写老前缀不会有任何报错,配置悄悄失效,连的是默认 localhost。这种悄悄失效比编译错误贵得多,全局搜一遍老前缀是必修课。 - profiles 加载规则。 Spring Boot 2.4 重写了配置文件处理,
spring.profiles.active只能写在主文档里,写进 profile 专属文件(比如 application-dev.yml)直接报错。这个项目运气好,active 写在主 application.yml 里,没踩上。建议每个要迁移的项目都查一遍这个位置。 - MultipartConfigElement。 项目里有个手工配置文件上传临时目录的 Bean,import 从
javax.servlet.MultipartConfigElement换到jakarta.servlet.,代码一行不用动------整个 jakarta 迁移里少有的纯改 import 的活,珍惜它。 - 没踩坑的部分也说一句。 spring-boot-starter-web / aop / data-jdbc / thymeleaf / amqp / data-redis 这些官方 starter,换完 parent 就完事了;
@Scheduled、@Async、RestTemplate 的行为都没变。迁移的恐怖故事里 90% 是依赖问题,Spring 自家的东西反而最省心。
上线前的自查清单
| 检查项 | 怎么搜 | 危险信号 |
|---|---|---|
| javax 残留 | grep -r "^import javax\." src/ |
剩下的只能是 crypto / net.ssl / naming / sql 这几个 Java SE 包 |
| springfox 尸体 | grep -i springfox pom.xml |
有任何一行 = 没迁完 |
| Druid / MyBatis-Plus 坐标 | pom.xml | starter 名字里带 -3- / -boot3- 的才是 Boot 3 线 |
| MySQL 驱动坐标 | pom.xml | mysql:mysql-connector-java 已永久停更 |
| 外置容器版本 | 问运维 | Tomcat 9 及以下装不了 jakarta 的 war |
| 老配置前缀 | grep -r "spring.redis" src/main/resources/ |
悄悄失效,不报错 |
| 拦截器 pattern | 找 WebMvcConfigurer 实现类 | 含中间段通配或后缀匹配的 pattern 重点测 |
| 文档地址 | 启动后访问 | /v2/api-docs 404、/v3/api-docs 200 才算换完 |
老炮点评
这次推演我最大的感受是:升级的成本大头,从来不在"升"本身。
四百多个编译错误是推演量级,九成是机械劳动,OpenRewrite 一晚上能跑完。真正烧时间的全是没被记录在案的技术债:一个 2020 年就死了的 Swagger 框架、一批自己改了名的 starter、一台跑在机房里没人敢动的 Tomcat 9、一条没人用但也没人敢删的离线文档生产线。
这些债有一个共同点:账是 2018 到 2022 年每年欠一点攒下来的,2026 年只是还款日。2.1.0 发布的那个月跟进升级,成本是一个下午;攒到 2.x 全系列报废再升,成本两周起步,外加一台 Tomcat 和无数杯咖啡。
所以我的建议就一条:大版本发布后一年内升到当时的稳定次新线,把升级变成日常维护,别攒成专项工程。技术债的利息按年复利计算,这话我在之前的文章里说过一次,这次推演又把它重复了一遍。
下期预告 《实战篇:真升 Spring Boot 3.5.x,我踩了哪些坑》
推演篇排雷,实战篇踩雷。parent 改 3.5.x、JDK 上 17、
mvn clean compile、OpenRewrite、springfox → springdoc、Druid/MyBatis-Plus/PageHelper 换坐标、外置 Tomcat 10------推演里标的【需编译验证】,下一篇逐个交作业。真炸了哪些,虚惊哪些,推错哪些,都会写。
如果本文对你有帮助,欢迎:
👍 点赞 | ⭐ 收藏 | 👤 关注| 💬 留言
你的每一次鼓励都是我继续更新的动力,我们下一篇见!🚀
我是老炮,18 年 Java 老兵,仍在一线。更多迁移实战在公众号「Java老炮踩坑录」。关注我不错过每一篇真实案例,少踩坑。