Skip to content

一次未完成的 Spring Boot 升级:从 2.2.10 到 2.5.14 的工程复盘 ​

最近,我开始重新整理过去的 Wiki 文档和 Git 提交记录,回顾此前做过的一些技术探索和工程改造。

在整理过程中,AI 提醒了我一件事:一次没有最终上线的技术升级,其探索过程同样值得记录。

这让我想起了 2025 年底曾经尝试过的一次 Spring Boot 升级。

当时,我尝试将一个长期运行的业务系统从 Spring Boot 2.2.10.RELEASE 升级到 2.5.14。实验最终没有进入生产环境,但留下了一个编号为 V14 的开发分支。

于是,我重新查看历史代码、Maven 配置和 Git 提交记录,尝试还原当时的升级过程。

起初,我和 AI 采用的是比较简单的协作方式:AI 提出 Git 命令,我在本地执行,再将结果提供给 AI 分析。

后来,由于需要检查的提交越来越多,我改为使用 Codex 直接对本地 Git 仓库进行只读调查。

随着调查深入,我逐渐发现,仅仅依靠 Git 历史并不能完整解释过去发生的事情。

例如,V14 为什么会出现?部分兼容性修改为什么提前进入生产?模块化治理究竟是在升级之前还是之后开始的?

这些问题,有的能够从 Git 中找到答案,有的需要结合我自己的开发经历进行回忆,还有一些即使经过讨论,仍然无法完全确认。

这次复盘实际上由三个方面共同推动:

  • AI 的提醒和追问:帮助我发现值得记录的经历,并提出过去没有主动思考的问题。
  • 我自己的回忆和判断:补充当时的开发背景、工作方式和技术决策。
  • Git、Wiki 等历史记录:提供可以核查的代码事实,也用来验证或纠正我的回忆。

三者都有各自的局限。

Git 能证明代码发生过什么变化,却未必能解释为什么变化;个人回忆能够补充背景,但可能遗忘细节;AI 能提出假设和分析线索,也可能得出不准确的解释。

因此,本文并不试图宣称已经完整还原了当年的全部过程。

我更希望通过现存证据、历史回忆和技术分析之间的交叉验证,尽可能准确地记录这次没有完成的升级实验。

本文在叙述中区分可以通过资料验证的事实、我对开发过程的回忆,以及仍需验证的技术推断。对于重要结论,会直接说明其证据来源或不确定性,而不为每一段机械地添加分类标签。

脱敏说明: 本文涉及的项目名称、模块名称及 Git 提交标识均已进行展示层脱敏。真实 Git Hash 统一替换为 BASE-01、UPG-01、FIX-01、MOD-01 等编号。这些编号仅用于本文内部关联,并非真实 Git SHA,不能直接用于 Git 命令查询。

相关技术版本、代码变化和提交之间的关系则尽可能依据原始记录保留,以便说明当时的工程情况。

一、为什么要升级一个仍在正常运行的系统? ​

1.1 当时的技术基线 ​

项目是一个持续运行和维护的 Java 业务系统,涉及订单、支付、渠道对接,以及多个内部系统之间的数据交互。

当时主要使用以下技术栈:

组件原有版本
Java8
Spring Boot2.2.10.RELEASE
Spring CloudHoxton.SR12
Dubbo2.7.8
Nacos Server2.0.3
Elasticsearch Server7.6.2(复盘时核实,历史版本待确认)
构建工具Maven

系统主要包含两个应用:

  • project-payment-console:面向运营人员的后台。
  • project-payment-portal:面向终端用户的前台。

此外,系统还需要与多个内部系统进行服务调用和数据交互。

从业务角度看,原有技术栈仍然能够支撑日常开发和运行。

但从长期维护的角度看,我并不希望一直停留在 Java 8 和 Spring Boot 2.2.10 上。

1.2 我的目标并不是停留在 Spring Boot 2.5 ​

Spring Boot 2.2.10 发布于 2020 年 9 月。

到了 2025 年底,它已经是一个发布五年多的旧版本,而且早已结束常规开源维护。

我希望逐步使用更新的 Java API、框架能力和依赖生态,也希望有机会获得更好的性能与长期维护条件。

我的长期目标甚至是 Java 25 和 Spring Boot 4.x。

但对于一个仍在运行的遗留系统,我并不打算一步完成这样的跨越。

项目中存在不少历史依赖,还需要与其他内部系统进行交互。直接跨越多个 Spring Boot 大版本,意味着需要同时面对大量兼容性变化。

因此,我当时选择先尝试一个中间版本:Spring Boot 2.5.14。

Spring Boot 2.5.14 于 2022 年 5 月发布,是 2.5 系列最后一个常规开源版本。随着该版本发布,2.5 系列结束了常规开源支持。

因此,到了 2025 年底,将 2.5.14 作为长期升级方案,已经不能解决技术栈持续维护的问题。

另外,Spring Boot 3.0 迁移指南早已建议,原有 Spring Boot 2.x 项目在迁移到 3.0 之前,先升级到最新的 2.7.x。

最后一个 Spring Boot 2.7 开源版本为 2.7.18。

这意味着,如果从当时的技术生态重新评价,2.7.x 比 2.5.14 更值得考虑为通往 Spring Boot 3.x 的阶段性版本。

那么,我当时为什么选择 2.5.14,而不是 2.7.18?

目前我已经无法准确还原最初的选型过程。

历史记录中确实出现过 Spring Boot 2.7.3 的相关线索,但这些线索不足以证明我当时的完整尝试顺序,也不能据此补写一段未经证实的版本选择过程。

能够确认的是,最终保留下来的实验分支采用 Spring Boot 2.5.14。

另外,2.5.14 仍然支持 Java 8。这使得实验能够暂时保留原有 Java 运行时,将主要调整范围放在 Spring Boot、Spring Cloud 及相关依赖组合上。

1.3 这是一项自主开展的技术探索 ​

这次升级并不是一项要求在指定日期完成的正式交付任务。

我是在完成正常业务开发之外,利用可支配的时间进行研究。

如果有新的业务功能、渠道接口或生产问题需要处理,我仍然会优先完成这些工作。

有时间时,再继续研究框架升级。

这种背景也决定了我的实验方式。

我没有一开始就建立一份完整的升级验收计划,而是先尝试修改依赖、启动应用、处理兼容性问题。

如果某个修改能够同时适用于 Spring Boot 2.2.10 和 2.5.14,我就希望先完成验证,并尽早将其合入生产主线。

这样,即使整体升级最终没有完成,已经验证过的局部修改仍然可以保留下来。

二、升级策略:为什么最终出现了 V14? ​

2.1 先解释当时的 Git 分支管理方式 ​

理解 V14 之前,需要先了解我当时的开发习惯。

项目主要使用以下分支:

  • prod:生产分支。
  • test:测试分支。
  • feat-xxx:独立功能开发分支。

开发新功能时,我通常从 prod 创建一个 Feature 分支。

例如:

text
prod
 ├── feat-channel-a
 ├── feat-channel-b
 └── feat-channel-c

三个 Feature 分别承载不同需求。

开发过程中,我会在对应的 Feature 分支上持续提交代码。

当功能具备测试条件时,再将相关提交 cherry-pick 到 test,随后部署测试环境进行验证。

如果测试发现问题,我仍然会回到原 Feature 分支继续修改,然后将新增提交 cherry-pick 到 test。

测试完成、满足上线条件后,再将该 Feature 的完整变更集成到 prod。

这里有一个重要区别:

我使用 cherry-pick,并不是为了从一个 Feature 中随意挑选某几个提交,而是为了按照提交顺序,将这个 Feature 所需的完整变更应用到目标分支。

2.2 为什么不直接 merge 测试分支? ​

这与项目当时的实际发布情况有关。

假设有三个渠道开发任务:

  • 渠道 A 已经完成开发并通过测试。
  • 渠道 B 已经完成开发并通过测试。
  • 渠道 C 也已经完成开发并通过测试。

但由于商务安排或者业务优先级发生变化,A、B 暂时不准备上线,只有 C 需要发布。

此时,test 可能已经包含三个渠道的代码,而 prod 尚未包含这些修改。

如果直接将 test merge 到 prod,就可能把 A、B 两个暂不准备发布的功能一起带入生产。

因此,我不能简单地将测试分支作为生产分支的直接合并来源。

我采用的方式是:每个 Feature 独立开发,分别将其完整变更 cherry-pick 到需要集成的分支。

例如:

text
feat-channel-a ── cherry-pick ──> test
feat-channel-b ── cherry-pick ──> test
feat-channel-c ── cherry-pick ──> test

feat-channel-c ── cherry-pick ──> prod

图中的 cherry-pick 表示按照正确顺序应用该 Feature 所需的提交序列,并不是任意选择单个提交。

我希望集成的是某个 Feature 的完整功能,而不是测试分支上积累的所有代码。

2.3 cherry-pick 不意味着可以忽略提交依赖 ​

假设一个 Feature 在开发过程中产生了十次提交:

text
Commit 1  增加基础接口
Commit 2  增加请求对象
Commit 3  增加 HTTP 客户端
Commit 4  增加签名验签
...
Commit 10 完成业务处理

这些提交可能存在明确的前后依赖。

如果只 cherry-pick 第十次提交,却没有引入前面的基础接口、对象和组件,那么代码可能无法编译,也可能在运行时出现问题。

因此,我通常会按照原有顺序,将该功能所需的完整提交序列应用到目标分支。

至于如何把一次开发组织成职责清楚的多个提交,我在后文介绍 AI 辅助 Git 提交时再展开。

2.4 为什么进入生产时,我仍然倾向于 cherry-pick? ​

从 Git 的技术能力来看,Feature 是从 prod 创建的,当然可以 merge 回 prod。

直接合并一个未混入其他需求的独立 Feature,也不会凭空引入测试分支中的其他功能。

不过,我在实际开发中已经形成了一套相对统一的 cherry-pick 集成习惯。

尤其是当某个 Feature 持续开发较长时间时,prod 可能已经发生了不少变化。

其他开发人员持续提交业务代码,我自己也可能正在进行公共类迁移、包结构调整或者 Maven 模块拆分。

我更倾向于将该 Feature 的完整提交序列重新应用到当前生产主线上,而不是为每次集成额外产生 merge commit。

当然,cherry-pick 并不能避免全部冲突。目标分支发生变化后,仍然可能出现文本冲突或运行层面的兼容性问题。

它也有自身的代价。

cherry-pick 通常会生成新的提交哈希。同一个逻辑修改在 Feature、test 和 prod 中可能对应不同的提交 ID。

如果后来删除原始 Feature 分支,又没有保留提交来源关系,那么事后追踪某项修改的来龙去脉就会更加困难。

这一点也与本次 V14 历史调查遇到的问题有关。

2.5 小步开发、独立验证、尽早集成 ​

长期处理这样的开发任务后,我逐渐形成了一个习惯:

小步开发、独立验证、尽早集成、按需启用。

我并不希望一个已经开发和验证完成的功能,因为整个大需求尚未结束,就一直停留在独立 Feature 分支中。

例如对接一个银行渠道,可能需要完成汇率查询、开户、付款、退款、回调等多个接口。

我通常不会等所有接口全部开发完成后,才进行第一次集成。

我可能先完成汇率查询。

虽然从业务上看只有一个接口,但它已经涉及配置管理、HTTP 调用、加密解密、签名验签,以及对应的 UT、IT 和测试数据。

当这个最小功能链路完成验证,并确认能够安全地与现有系统共存后,我就希望先将它集成到生产主线。

至于业务是否正式启用,则可以通过相应配置和功能入口进行控制。

当然,代码进入生产分支并不等于可以忽略生产验证。即使业务入口暂时关闭,也需要确认新增代码不会在应用启动、定时任务或其他运行路径中产生意外影响。

这种开发习惯还有一个实际收益:

缩短 Feature 与生产主线之间的偏离时间。

尤其是在遗留系统持续进行模块化调整时,今天可能迁移一个类,明天可能调整一个包,后天又拆出新的 Maven 模块。

一个 Feature 长期不进入生产主线,未来再集成时可能产生更多适配工作。

所以,我更愿意将已经完成验证的阶段性成果及时纳入主线,而不是让大量完成的代码长期停留在不同的 Feature 分支中。

2.6 这套习惯也影响了 Spring Boot 升级 ​

Spring Boot 升级实验采用了类似的思路。

如果我在新版本中发现某个兼容性问题,而修复方式同时适用于旧版本,那么就可以先处理这个局部问题。

验证没有破坏 Spring Boot 2.2.10 的现有行为后,将修改合入 prod。

然后继续推进升级实验。

这与直接将 Spring Boot 2.5.14 整体发布到生产环境完全不同。

前者是独立完成验证的局部兼容性调整;后者涉及整个应用的运行环境和依赖组合,需要更广泛的验证。

2.7 V14 更可能是分支演进编号,而不是十四次失败 ​

现存升级实验分支最终编号为 V14。

最初重新调查 Git 历史时,我曾经尝试按 V1~V14 推断每一轮分别解决了什么技术问题。

但回顾自己的日常开发习惯后,我逐渐意识到,这个理解可能并不准确。

当时生产主线持续变化。

其他业务功能不断进入生产,部分兼容性修复也可能提前合入 prod。

为了让升级实验始终基于相对新的生产代码,我很可能多次放弃旧实验分支,再从最新的生产基线重新建立分支。

这与我日常采用的小步开发方式相吻合。

因此,我现在更倾向于认为,V14 主要反映实验分支的多次重建,而不是十四次独立的兼容性故障。

但这仍然只是根据我的开发习惯、历史回忆和现存 Git 记录得出的推断。

早期分支已经清理,我无法逐一证明 V1~V13 为什么建立。

所以,本文不会将 V14 描述成十四次升级失败,也不会为这些编号编造已经找不到的故障过程。

三、重新调查 Git:我究竟保留了什么? ​

3.1 V14 并不是只有一个升级提交 ​

通过 Codex 对现存 Git 历史进行调查,我确认最终保留的 V14 分支包含四个独有提交。

它们的关系如下:

text
BASE-01
    │
    └── UPG-01  Spring Boot 2.5.14 升级补丁
          │
          └── UPG-02  Nacos Discovery 依赖调整
                │
                └── UPG-03  Elasticsearch 相关修改
                      │
                      └── UPG-04  Portal CORS 修改

以上是经过脱敏的提交关系示意图,编号与原始 Git 历史中的提交对应,但不代表真实 Git Hash。

其中,UPG-01 是核心框架升级提交。

后面三个提交分别涉及 Nacos Discovery、Elasticsearch 和 Portal CORS。

这些代码能够证明当时确实继续处理过相关模块的适配。

不过,仅凭这些提交还不能确定对应修改在当时分别经过了哪些运行验证。

3.2 AuthorDate 和 CommitDate 为什么不同? ​

调查中还发现一个值得注意的细节。

核心升级提交 UPG-01 的日期为:

text
AuthorDate: 2025-12-25
CommitDate: 2026-03-03

而它的直接父提交 BASE-01,已经是 2026 年 3 月 2 日的生产代码基线。

这与我平时从较新的生产分支重新建立实验分支、再应用已有修改的开发方式相吻合。

因此,我推测 V14 可能经历过类似的分支重建或补丁重新应用过程。

不过,具体每次重建发生在什么时候、出于什么原因,仍然无法仅凭现存提交日期确认。

Git 保存了当前可见的代码关系,但并没有完整保留我当时每一次调整实验基线的原因。

四、技术改造:哪些问题能够找到代码证据? ​

虽然部分原始异常日志没有完整保留,但 Git 历史仍然留下了不少可以确认的代码修改。

这次复盘还补充了一项原先缺少的证据:在当前生产分支和 V14 实验分支分别执行 Maven Dependency Tree,取得了两个版本的实际依赖解析结果。

因此,本章涉及依赖版本时,会优先使用这两份解析结果,而不是只根据 POM 属性或 Spring Boot 默认版本进行判断。

4.1 Spring Boot 与 Spring Cloud 版本调整 ​

核心升级提交 UPG-01 对 Maven 依赖进行了较为集中的修改。

主要版本变化如下:

diff
- Spring Boot 2.2.10.RELEASE
+ Spring Boot 2.5.14

- Spring Cloud Hoxton.SR12
+ Spring Cloud 2020.0.6

+ Spring Cloud Alibaba BOM 2021.1

Dubbo 仍然保留在 2.7.8。

这说明我当时并没有尝试一次性更新所有基础组件。

我的想法是先调整 Spring Boot、Spring Cloud 及相关依赖组合,观察系统是否能够在保留 Java 8 和部分旧组件版本的情况下继续运行。

这里需要注意,Spring Boot 升级并不是简单修改一个 Maven 版本号。

它还会影响 Spring Framework、Spring Cloud、依赖管理、自动配置及相关第三方组件。

因此,即使能够编译成功,也不能直接说明升级已经完成。

4.2 OpenFeign:参数名称显式化 ​

这是目前能够找到比较明确证据的一次兼容性调整。

相关脱敏提交编号为 FIX-01。

原来的 Feign 接口存在这样的代码:

java
@RequestParam Long id

以及:

java
@PathVariable String orderId

后来调整为:

java
@RequestParam(value = "id") Long id
java
@PathVariable(value = "orderId") String orderId

改动本身很小。

但它与当时的升级实验有关。

根据我的回忆,原有 Spring Boot 2.2.10 及相关依赖组合下,未显式指定参数名称的写法能够正常工作。

但在升级实验的新依赖组合中,参数名称推断没有达到预期。

显式指定名称后,问题得到处理。

随后,我确认这种写法在旧版本下也能够正常使用,因此将该修改提前合入生产主线。

这正好体现了我当时的升级策略:

能够同时兼容新旧版本的局部修复,可以先完成验证并进入生产,而不必等待整套框架升级完成。

不过,这里不能直接得出“Spring Boot 2.5 必须为所有 @PathVariable 显式填写 value”的结论。

参数名称解析还可能受到 Spring Cloud OpenFeign、Java 编译参数以及具体接口声明方式的影响。

本次重新检查 POM 时,我确认新旧两个根 POM 都继承了 spring-boot-starter-parent,并且在 maven-compiler-plugin 中显式配置了:

xml
<parameters>true</parameters>

因此,不能简单地将问题归因于项目根 POM 没有开启 Java 参数名称保留功能。

如果以后重新调查,应该进一步检查具体子模块是否覆盖了编译插件配置、编译产物是否保留参数名称,以及当时使用的 Spring Cloud OpenFeign 版本和参数解析行为。

目前能够确认的是,显式填写注解参数名解决了当时遇到的问题,但完整触发条件尚未恢复。

4.3 Ribbon 到 Spring Cloud LoadBalancer ​

在 Console 和 Portal 的 Maven 配置中,我还能找到一项明确的依赖调整。

原来的服务调用配置使用:

xml
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>

升级分支则引入:

xml
<artifactId>spring-cloud-starter-loadbalancer</artifactId>

此外,Console 中还存在针对部分内部依赖的 Ribbon Starter 排除配置。

这与 Spring Cloud 2020.0 系列的组件演进有关。

当时升级 Spring Cloud 后,原有 Ribbon 相关依赖需要重新处理。

从 Git 和 POM 中能够确认的是,Console 和 Portal 的依赖声明确实发生了变化。

但这只能证明依赖层面的适配,不能替代服务调用的端到端验证。

4.4 Nacos:通过 Dependency Tree 核实实际版本 ​

V14 中有一笔单独的 Nacos Discovery 依赖修改:

UPG-02

该提交整理了 Discovery 相关依赖配置。

最初调查时,我主要通过 POM 和 BOM 判断依赖变化,但仅凭版本声明并不能确定 Maven 最终使用哪个 Nacos Client。

为此,我在本次复盘中分别切换到当前生产分支和 V14 实验分支,执行 Maven Dependency Tree。

升级前后的实际依赖版本 ​

最终得到以下结果:

组件当前生产分支V14 实验分支
Nacos Discovery Starter2.2.3.RELEASE2021.1
Nacos Client1.3.31.4.1
Nacos Common1.3.31.4.1
Nacos API1.3.31.4.1

因此,这次实验中能够通过 Maven Dependency Tree 确认的 Nacos Client 版本变化为:

1.3.3 → 1.4.1

当前生产分支的主要依赖路径为:

text
common-biz
└── common-infra
    └── nacos-discovery-starter:2.2.3.RELEASE
        └── nacos-client:1.3.3

V14 在包含相关 Reactor 模块的依赖解析中,公共模块下显示了 Discovery Starter 和 Nacos Client:

text
common
├── nacos-discovery-starter:2021.1
└── nacos-client:1.4.1

以上省略了完整 GroupId,并统一使用脱敏模块名称。

本次复盘以最终 Maven 依赖解析结果作为版本对照依据,不再根据历史遗留的 Properties 配置推断实际使用的客户端版本。

为什么第一次依赖树出现了不属于 V14 的模块? ​

这次调查还有一个值得记录的细节。

第一次单独检查 V14 的 Portal 模块时,依赖树中出现了后来模块治理产生的 common-biz 和 common-infra。

但这些并不是 V14 当时的公共模块结构。

我怀疑 Maven 使用了本地仓库中同版本号的内部构件。

项目的部分内部 Maven 构件采用固定版本号,而不是 SNAPSHOT 版本。

因此,如果不同分支中使用了相同的内部构件版本号,而本地仓库已经存在其中一个分支安装的构件,就可能出现依赖路径与当前源码模块结构不一致的问题。

随后,我重新执行 Maven Dependency Tree,并增加 -am 参数,让相关 Reactor 模块一起参与解析。

这次依赖路径恢复为 V14 当前源码对应的模块结构。

不过,Nacos Client 仍然解析为 1.4.1。

因此,这次检查纠正的是依赖路径,而不是 Nacos Client 的最终版本。

这也是我后来更加重视 Maven 实际解析结果的原因。

同一个项目中,POM 声明、传递依赖以及本地仓库中的内部构件,都可能影响对最终依赖关系的判断。

Spring Cloud Alibaba 的版本组合 ​

Spring Cloud Alibaba 官方版本说明中,2021.1 对应的参考组合为:

  • Spring Cloud:2020.0.1
  • Spring Boot:2.4.2

而 V14 实际使用的是:

  • Spring Cloud Alibaba:2021.1
  • Spring Cloud:2020.0.6
  • Spring Boot:2.5.14

因此,V14 的版本组合并非官方版本对应表中列出的组合。

这不等于一定存在兼容性故障,但如果重新开展实验,值得优先检查。

历史 gRPC 异常 ​

此前的历史记录中,还出现过 Nacos gRPC 连接检查超时、客户端未完成连接等异常线索。

但本次依赖树确认,当前生产分支与 V14 分别使用 Nacos Client 1.3.3 和 1.4.1,都属于 Nacos 1.x 系列。

Nacos 2.x 客户端才引入基于 gRPC 的长连接机制。

此外,Nacos 官方说明 2.0.x 服务端可以兼容 1.x 客户端,因此 Server 2.0.3 与 Client 1.4.1 在通信协议层面存在官方兼容依据。

所以,不能将此前的 gRPC 异常直接归因于这两个已经确认的客户端版本组合。

相关异常可能来自其他版本实验或不同的实际运行环境,需要另行调查。

4.5 CORS 与 Spring Security 的适配 ​

升级 Spring Boot 2.5.14 的过程中,我遇到了服务无法正常启动的问题,并对 Console 和 Portal 的 CORS 配置进行了调整。

原有 Console 使用 CorsFilter Bean 配置跨域,其中同时存在:

java
config.addAllowedOrigin("*");
config.setAllowCredentials(true);

升级后,我将这部分配置调整为 WebMvcConfigurer.addCorsMappings,使用 allowedOriginPatterns("*"),并保留允许凭证的配置。

与此同时,还调整了允许的 HTTP 方法、预检缓存时间,以及部分 Spring Security 配置。

这些变化能够从脱敏提交 UPG-01 的代码差异中确认。

Spring Framework 5.3 对 allowCredentials(true) 与通配来源配置增加了校验限制,两者不再允许直接通过 allowedOrigins("*") 组合使用。

这为当时的修改提供了明确的技术依据。

另外,Console 原有 Security 配置中,曾将 CorsFilter 插入 Spring Security 过滤器链。

升级提交移除了这部分旧 Filter 的注入及插入代码,同时取消了 CrossInterceptor 的组件注册。

这里的 CorsFilter 是过滤器,而 CrossInterceptor 是拦截器,两者属于不同的组件。

这说明当时进行的并不只是一次 CORS 方法名称替换,而是对原有 CORS 接入方式进行了一定调整。

Portal 后续也进行了对应修改:

diff
- .addAllowedOrigin("*")
+ .addAllowedOriginPattern("*")

这项修改保留在提交 UPG-04 中。

根据我的回忆,当时进行这些修改的直接目的是解决服务无法正常启动的问题。

现存代码能够确认旧配置确实包含凭证与通配来源的组合,但原始异常堆栈没有完整保留,因此没有进一步还原具体异常触发位置。

对于本文而言,记录这次为适配 Spring Framework 5.3 而进行的 CORS 修改,就已经足够。

4.6 Portal 与 Elasticsearch 的适配 ​

根据我的回忆,当时 Console 的适配推进得比较早。

后来继续处理 Portal 时,我遇到了 Elasticsearch 相关的问题。

现存提交 UPG-03 保留了 Elasticsearch 实体、字段映射、标识字段以及索引相关调整。

最初调查时,我注意到 Spring Boot 2.2.10 与 2.5.14 默认管理的 Spring Data Elasticsearch 版本不同。

前者为 3.2.10.RELEASE,后者为 4.2.11。

Spring Data Elasticsearch 官方迁移文档记录了从 3.2.x 到 4.x 期间的一系列变化,包括实体映射方式、查询接口及部分 API 调整。

同时,Spring Boot 2.2.10 默认管理的 Elasticsearch REST High Level Client 是 6.8.12,而 Spring Boot 2.5.14 默认管理的是 7.12.1。

但我在两个项目根 POM 中都找到了:

xml
<elasticsearch.version>7.6.2</elasticsearch.version>

因此,仅根据 Spring Boot 默认依赖版本,并不能准确判断项目最终使用哪个 Elasticsearch 客户端。

通过 Maven Dependency Tree 确认实际版本 ​

在本次复盘中,我分别在当前生产分支和 V14 分支执行了 Maven Dependency Tree。

结果如下:

组件当前生产分支V14 实验分支
Spring Boot Data ES Starter2.2.10.RELEASE2.5.14
Spring Data Elasticsearch3.2.10.RELEASE4.2.11
Elasticsearch REST High Level Client7.6.27.6.2
Elasticsearch REST Client7.6.27.6.2
Elasticsearch Core7.6.27.6.2
Elasticsearch Transport Client7.6.2本次依赖树未出现

当前生产分支的主要依赖关系为:

text
spring-boot-starter-data-elasticsearch:2.2.10.RELEASE
└── spring-data-elasticsearch:3.2.10.RELEASE
    ├── transport:7.6.2
    └── elasticsearch-rest-high-level-client:7.6.2
        └── elasticsearch-rest-client:7.6.2

V14 的主要依赖关系为:

text
spring-boot-starter-data-elasticsearch:2.5.14
└── spring-data-elasticsearch:4.2.11
    └── elasticsearch-rest-high-level-client:7.6.2
        └── elasticsearch-rest-client:7.6.2

这两份依赖树解决了此前最重要的版本疑问。

Spring Data Elasticsearch 确实从 3.2.10.RELEASE 升级到了 4.2.11,但 Elasticsearch REST 客户端仍然保持 7.6.2。

另外,当前生产分支的依赖树中存在 Transport Client,而 V14 的本次依赖树没有出现。

但依赖树包含 Transport Client,并不代表业务代码实际通过它访问 Elasticsearch。是否使用还需要通过具体客户端配置和调用代码判断。

为什么这个版本组合值得关注? ​

从 Spring Data Elasticsearch 官方版本对应关系来看:

Spring Data Elasticsearch官方对应 Elasticsearch 版本
3.2.x6.8.12
4.0.x7.6.2
4.1.x7.9.3
4.2.x7.12.0

由此可见,当前生产分支的 Spring Data Elasticsearch 3.2.x 搭配客户端 7.6.2,本身就与官方版本对应表不同。

而 V14 的客户端 7.6.2,恰好是 Spring Data Elasticsearch 4.0.x 官方对应的版本,但 V14 实际采用的 Spring Data Elasticsearch 已经是 4.2.11。

因此,这次升级留下了一项比较明确的兼容性调查方向:

Spring Data Elasticsearch 4.2.11 与 Elasticsearch REST Client 7.6.2 之间,是否存在客户端 API 或运行行为方面的不兼容?

另外,Spring Data Elasticsearch 4.0 开始弃用基于 TransportClient 的相关支持,推荐向 REST 客户端迁移。

不过,4.x 开始弃用 TransportClient,并不等于所有 4.x 版本都已经彻底删除了该组件。

结合官方迁移文档,更值得关注的是:

  • 从 3.2.x 到 4.0.x,实体映射、查询接口等方面发生了哪些变化?
  • 从 4.0.x 到 4.1.x,再到 4.2.x,哪些 API 开始依赖更新版本的 Elasticsearch 客户端?
  • 项目原有实体和索引代码是否需要适配这些变化?

从提交 UPG-03 保留的修改内容来看,这些方向都值得检查。

不过,依赖树只能证明 Maven 最终选择了哪些 JAR,不能证明当时具体发生了什么异常。

因此,我现在可以明确记录版本组合,但仍然不能将某个版本冲突写成已经确认的故障根因。

Elasticsearch 服务端版本 ​

本次复盘中,我还检查了 Elasticsearch 服务端。

服务返回的信息显示:

text
Elasticsearch Server: 7.6.2
Build Type: Docker
Lucene: 8.4.0

这里的版本来自本次实际检查,而不是当年的部署记录。

它与当前生产分支和 V14 的客户端版本一致,但不能仅凭这次响应,就倒推出 2025 年底实验时的服务端版本。

如果以后重新开展升级实验,应当结合当时或重新搭建的服务端环境,验证实际客户端版本与 Spring Data Elasticsearch 之间的兼容性。

4.7 不是所有同期修改都属于框架升级 ​

Git 调查中还发现了一些 DTO、Lombok、序列化及 Maven 编译配置的修改。

例如 FIX-02,其提交说明中提到了方便升级 Spring Boot 2.5.x。

部分 DTO 调整了 Lombok 构造器注解,某些类型增加了 Serializable。

后续 FIX-03 又恢复了部分构造器注解。

这些修改确实存在,但并不足以证明 Spring Boot 2.5 与 Lombok 必然存在不兼容问题。

此外,项目在同一时期还进行过数据库访问层、连接池以及公共组件的治理。

这些工作可能改善系统维护条件,但不能因为时间相近,就全部归因于 Spring Boot 升级。

五、为什么最终没有进入生产? ​

5.1 局部修复和整体升级是两回事 ​

回头看,这次实验中有一些修改确实可以独立验证。

例如 Feign 参数显式化。

它可以先在旧版本正常运行,再进入生产主线。

但整个 Spring Boot 版本切换并不是同一回事。

要真正切换至 2.5.14,还需要考虑应用自身的完整运行,以及与其他内部系统之间的交互。

根据我的回忆,当时 Console 大概率已经能够启动,也进行过部分接口调用验证。

但微服务之间的调用究竟验证到了什么程度,我现在没有把握确认。

因此,我不能把应用启动成功写成整个系统已经完成升级验收。

5.2 跨系统调用属于验证范围,但不能直接当成停止原因 ​

项目需要与多个内部系统进行数据交互和服务调用。

这意味着,升级实验除了检查本项目内部功能,还需要考虑既有服务调用是否保持正常。

不过,Spring Boot 版本升级本身并不会自动改变既有的 Dubbo 协议或 Feign 接口契约。

真正需要关注的是框架和依赖组合变化可能引起的运行行为差异。

例如,Spring Cloud LoadBalancer 替换 Ribbon 后,服务选择和调用流程是否正常;Nacos 客户端和 Discovery 依赖变化后,服务注册与发现是否受到影响。

这些都属于可以设计测试进行验证的问题。

但目前没有完整的测试记录,能够证明当时具体是哪一项跨系统验证阻碍了升级。

所以,它们可以被列为升级需要覆盖的验证范围,却不能直接写成项目停止推进的原因。

5.3 V14 只是最后留存的实验分支 ​

我并没有正式决定将 V14 作为升级实验的最终版本。

它只是当时最后保留下来的分支。

如果后来还有时间继续推进,我完全可能重新创建 V15、V16,或者使用其他方式继续实验。

但项目还有日常业务开发,我也在持续进行模块化和公共代码治理。

后来,这项升级实验没有再继续推进。

现在重新评价,Spring Boot 2.5.14 已经停止常规开源维护、长期迁移仍需继续升级,以及不同框架版本组合需要进一步验证,都是值得考虑的现实因素。

但我不能据此倒推出,这些就是当时停止实验的直接原因。

至于当时究竟是因为时间安排、版本选择、验证范围,还是继续投入的收益判断,现在已经很难准确区分。

因此,我更愿意将这次经历描述为:

一次逐渐搁置、最终没有进入生产的框架升级探索,而不是一次已经证明技术上不可行的升级失败。

六、与升级实验并行的内部模块治理 ​

6.1 模块治理早于升级实验,并与之并行 ​

这次调查还纠正了我对历史过程的一种猜测。

最初,我曾经怀疑,是不是因为 Spring Boot 升级涉及较多因素,后来才逐渐将更多精力投入内部模块调整。

但 Git 记录并不支持这种简单的先后关系。

早在 2025 年 11 月,项目就已经出现旧模块清理。

2025 年 12 月,又进行了 Maven 模块命名、目录迁移和公共组件整理。

同一时期,我也在开展 Spring Boot 升级探索。

这说明模块治理早于最终保留的升级实验,并且与升级工作存在时间上的重叠。

两项工作原本就在并行推进,而不是 Spring Boot 升级失败以后才开始模块治理。

6.2 2026 年 5~7 月的公共模块拆分 ​

虽然内部治理早已开始,但 Git 记录显示,2026 年 5~7 月确实出现了一轮比较集中的公共模块拆分。

原有公共模块逐步整理为职责更明确的 Maven 模块,例如:

text
project-payment-commons
├── common-core
├── common-utils
├── common-mapper
├── common-biz
├── common-pojo
├── common-infra
├── common-integrations
└── common-slslogger

我进行这种调整,并不是为了追求更多的 Maven 模块。

真正的目标是让代码归属与依赖方向更加明确。

例如:

DateUtils、FileUtil 及相关测试进入工具模块。

Mapper 接口、XML 等数据访问组件进入相应模块。

请求响应对象、DTO 等按照职责重新整理。

相关基础设施能力也根据实际职责进入对应模块。

这些修改都可以通过历史 POM 和文件迁移记录进行验证。

但模块数量增加,并不能自动证明架构质量已经提高。

真正需要关注的是代码职责是否更加清晰、依赖方向是否合理,以及相关功能能否继续通过验证。

6.3 为什么内部治理可以持续推进? ​

相比整套 Spring Boot 升级,内部模块治理通常更容易限定修改范围。

例如移动一个工具类,我可以明确原始位置、目标位置、相关依赖和对应测试。

完成迁移后,通过编译、UT 或必要的集成验证,确认原有行为没有变化,再将修改及时合入生产主线。

这样的工作更适合小步推进。

当然,这并不意味着模块拆分没有风险。

但相比需要考虑完整框架依赖组合的升级,部分内部调整能够建立相对明确的修改和验证边界。

从 Git 历史看,Spring Boot 升级实验没有继续推进之后,这类治理仍然持续开展,并在后续形成了比较集中的模块拆分。

但我不能据此推断自己当时作出过一次明确的“放弃框架升级、转向模块治理”的决策。

两项工作本来就同时存在,只是后来继续推进的程度不同。

6.4 UT/IT 是原有工程实践的逐步引入 ​

内部治理还包括 JUnit 5 迁移、UT/IT 命名及 Maven 测试执行配置整理。

不过,UT/IT 并不是我在这次 Spring Boot 升级过程中才设计出来的。

这套测试分类和执行边界,我此前已经在其他项目中使用多年。

后来,只是在合适的时间逐步将它引入当前系统。

相关设计已经单独写在《别把所有测试都叫 Test:我如何用 UT、IT 和 SmokeIT 划分执行边界》中,本文不再展开。

这里仅说明,模块化治理不仅涉及代码目录和 Maven 结构,也需要考虑如何保留和组织相应的测试能力。

七、Git 还在,为什么实验过程仍然难以还原? ​

7.1 代码记录与实验记录并不是一回事 ​

这次复盘中,最让我感到遗憾的事情,并不是 Spring Boot 最终没有升级成功。

而是重新查看历史时,我发现自己已经很难准确回答一些问题。

例如:

V2 为什么创建?

V3 为什么需要重新建立?

某次修改究竟针对什么异常?

当时只是编译成功,还是已经启动并调用过接口?

哪些修改提前进入了生产?

哪些修改只是实验过程中暂时采用的方案?

Git 中保留了部分代码和提交,却没有完整保留这些问题的答案。

尤其是多次 cherry-pick、重建分支以及删除早期分支之后,现存提交很难再还原全部实验过程。

这与我前面介绍的分支管理方式也有一定关系。

cherry-pick 能够帮助我控制代码集成范围,但默认不会保留一条容易直接追踪的原始提交关系。

重新建立实验分支能够使用较新的生产基线,却可能让早期实验过程随着旧分支清理而逐渐消失。

这不意味着应该简单否定原来的开发流程。

但它说明,我当时没有为这套流程建立足够完整的历史追踪机制。

7.2 如果重新设计 Git 记录方式 ​

现在回头看,我认为还可以改进提交来源的追踪方式。

例如,Git 提供了:

bash
git cherry-pick -x <commit>

对于没有冲突的 cherry-pick,-x 会在新提交的消息中追加来源提交的哈希。

这有助于以后查找不同分支之间的提交关系。

但它也有局限。

Git 官方文档说明,这种自动追加来源信息的行为针对无冲突的 cherry-pick。

而且,即使保留了来源哈希,如果原始提交失去所有引用并在仓库清理后被删除,也可能无法再直接查看原始提交内容。

对于需要长期保存的实验分支,我还可以考虑在删除分支前创建归档标签,例如:

bash
git tag archive/springboot2.5-v14 <commit>

这样可以为对应提交保留 Git 引用,避免重要的历史提交仅仅因为分支被清理而失去引用。

如果需要调查不同哈希的等价提交,还可以尝试使用:

bash
git cherry
git patch-id

另外,对于持续演进的升级实验,也可以考虑通过 rebase 将实验提交重新应用到较新的生产基线上,而不是每次都建立新的 V 号分支。

但 rebase 同样会改变被重写提交的哈希。

这些 Git 机制能够改善代码历史追踪,却不能自动恢复当时为什么创建分支、为什么修改依赖,以及具体完成了哪些验证。

因此,我认为真正缺少的并不是某一条 Git 命令,而是对实验过程的持续记录。

7.3 如果重新开展类似实验,需要记录什么? ​

现在回头看,我认为不需要为每次实验编写一篇长文档。

只需要保存少量关键信息:

记录内容需要回答的问题
实验基线本次基于哪个生产提交?
建立原因为什么创建或重建分支?
修改内容保留、增加或删除了哪些修改?
问题现象实际遇到了什么异常?
验证结果编译、启动、接口调用分别验证到什么程度?
后续处理修复进入生产、继续实验,还是暂时搁置?

如果当时能够持续保存这些信息,今天就不需要再根据 V14 和少量 Git 提交,反复推断过去的开发过程。

分支版本号可以帮助区分不同阶段,但不能代替实验记录本身。

八、AI 如何帮助我重新发现这段工程历史? ​

8.1 这篇文章最初并不在我的计划中 ​

这次复盘最初只是一次普通的 Wiki 和 Git 历史整理。

我原本希望回顾过去开发过哪些功能、完成过哪些工程改造,以及有哪些技术工作值得保留记录。

在这个过程中,AI 提醒我:

即使某次升级最终没有上线,探索过程中遇到的问题和已经完成的修改,仍然可能值得记录。

这让我重新想起了 Spring Boot 2.5.14 的 V14 分支。

如果没有这个提醒,我未必会专门回头调查这次实验。

所以,AI 在这篇文章中的第一个作用,并不是生成正文,而是提醒我发现一段值得重新审视的工程经历。

8.2 从手工 Git 调查,到让 Codex 直接分析仓库 ​

最开始,我和 AI 的协作方式比较简单。

AI 根据已有信息提出调查思路,再给出 Git 命令。

我在本地执行命令,将输出结果复制回来,AI 再继续分析。

这种方式可以工作,但当需要检查的提交、分支和文件越来越多时,往返成本也逐渐增加。

后来,我改为让 Codex 直接读取本地 Git 仓库。

我要求它只进行只读调查,不修改代码、不切换分支、不执行构建,也不对仓库进行提交操作。

它可以直接搜索历史提交、检查 Diff、比较分支关系,并尝试识别 cherry-pick 或重新应用补丁产生的等价修改。

这让原来需要反复手工执行的调查工作,能够更加系统地进行。

但更有意思的事情,并不是 AI 找到了多少提交。

而是调查结果不断让我重新思考过去的经历。

8.3 AI 的提问,让我重新发现自己已经遗忘的细节 ​

这次讨论中,最典型的问题是 V14。

最初,我们很容易把 V14 理解成十四轮技术问题排查。

但随着 AI 不断询问分支如何创建、为什么重建、兼容性修改如何进入生产,我开始联想到自己长期以来的开发习惯。

我平时就习惯小步开发、尽早集成,并且经常从最新生产基线创建新的 Feature 分支。

这让我意识到:

V14 可能并不意味着十四次技术失败,而是生产主线不断变化,导致升级实验分支被多次重新建立。

这个解释不是 AI 独立从 Git 中计算出来的。

它来自 AI 的追问、我的回忆,以及现存 Git 记录之间的相互印证。

另一个例子是模块化治理。

最初,我曾怀疑自己是不是在 Spring Boot 升级搁置后,才逐渐将更多精力投入内部模块调整。

但后来通过 Git 调查发现,模块治理实际上早于升级实验,两项工作一直存在交集。

这次,历史记录反过来纠正了我的推测。

还有 cherry-pick。

最初,AI 将我的操作理解成“从 Feature 中选择某几个提交发布”。

但这并不是我的真实意思。

我实际采用的是:将一个 Feature 所需的完整提交序列,按照顺序 cherry-pick 到目标分支,避免因为直接合并测试分支而带入其他不相关功能。

经过反复解释,最终才把这套开发流程还原清楚。

这些例子让我意识到:

AI 的价值不只是回答我已经提出的问题,有时还在于提出那些我原本没有意识到应该追问的问题。

某些事情并不是我完全不知道。

只是随着时间过去,我没有主动想起,也没有意识到它们可能影响对历史技术决策的理解。

而 AI 的追问,恰好帮助我重新建立了这些联系。

8.4 AI、个人回忆和历史记录,需要互相验证 ​

这次复盘也让我更加清楚地认识到三种信息来源的区别。

第一种是 Git、Wiki 等历史记录。

它们能够证明某个文件发生过修改、某个依赖版本发生过变化,或者某个模块在什么时候被拆分。

但 Git 并不会自动告诉我,当时为什么作出这个决定。

第二种是我自己的回忆。

我能够解释当时为什么使用 cherry-pick、为什么偏好小步集成,以及为什么将能够兼容旧版本的修改提前纳入生产。

但对于已经过去较长时间的具体异常、实验顺序和停止原因,我也可能记不清楚。

第三种是 AI 的分析。

AI 能根据现有信息提出解释,发现资料之间的联系,还能提醒我去寻找新的证据。

但如果它误解了开发背景,也可能得出错误判断。

因此,我不会因为某个解释来自 AI,就认为它正确。

同样,我也不会因为某件事来自自己的回忆,就认为完全不需要验证。

我更希望采用这样的方式:

AI 提出问题和假设,我补充经历与判断,再通过历史记录寻找支持或反驳的证据。

如果证据不足,就保留推断性质,而不是强行得出确定结论。

8.5 AI 还能帮助组织 Git 提交 ​

除了事后的历史调查,我后来在日常开发中还发现了 AI 的另一项实用能力。

以前开发一个功能时,我可能修改多个 Java 类、资源文件和测试代码。

等整个功能完成、验证通过后,再统一提交一次。

但后来使用 AI 辅助 Git 提交时,我发现,它有时能够根据代码之间的职责和依赖关系,将我原本准备一次提交的修改拆成多个逻辑清楚的提交。

例如,我曾开发过一个基于 Excel 的交易对账功能。

渠道方提供一份 Excel,通过 Type 字段区分不同交易类型,例如入账、VCC(虚拟信用卡支付)和 eCheck。

我需要解析这份 Excel,再分别查询自己系统中的交易记录,按业务类型进行比较,并识别双方数据中的差异。

从需求角度看,它可以被理解为一个完整的对账功能。

但从代码角度看,可能涉及渠道文件解析、内部数据查询、数据对象、比较逻辑以及对应测试。

以前我可能把这些修改统一提交。

而 AI 可以尝试将它们整理成更细的逻辑单元。

例如:

text
Commit 1:增加渠道 Excel 解析能力
Commit 2:增加内部交易数据查询能力
Commit 3:实现入账数据比对
Commit 4:实现 VCC 数据比对
Commit 5:实现 eCheck 数据比对

这里只是为了说明拆分思路,不代表当时 AI 一定按照这个顺序提交。

真正有价值的是,AI 能够分析这些修改之间的依赖关系,并尝试形成职责相对清晰的提交结构。

但我并不认为提交越多越好。

如果拆分破坏了功能完整性,或者中间提交无法编译,那么这种拆分反而会增加维护成本。

我更关注的是:

每个提交是否具有明确的修改目的,以及相关依赖是否得到正确保留。

这种能力与我原有的小步开发、cherry-pick 集成习惯并不冲突。

AI 可以帮助我组织一次开发中的变更,而一个 Feature 需要进入测试或生产时,我仍然会按照正确顺序集成它所需的完整提交序列。

8.6 如果 AI 能参与开发过程记录,结果可能会不同 ​

回到这次 Spring Boot 升级实验。

我现在最大的遗憾之一,是早期实验过程没有留下完整记录。

但如果把 AI 放到日常开发过程中,而不是等到事后才进行调查,情况可能会有所不同。

例如,每次重新建立升级分支时,让 AI 根据真实 Git Diff、编译日志、运行结果和我的说明,整理一份简短记录。

记录本次基于哪个生产提交,为什么重建分支,保留了哪些修改,解决了什么问题,还有哪些验证没有完成。

这些工作不需要 AI 替我作出技术决策。

它只需要把当时实际发生的事情整理下来,再由我检查和确认。

这样,即使后来删除了早期实验分支,仍然能够保留必要的技术背景。

所以,这次经历让我看到,AI 可以参与的工程工作其实是连续的:

从提出问题、分析方案,到辅助修改代码、组织提交,再到保存实验记录,以及事后的历史调查。

相比事后依靠大量 Git 提交推断过去发生了什么,我更希望把记录工作提前到开发过程中。

九、结语:一次没有上线的升级,留下了什么? ​

这次 Spring Boot 升级最终没有进入生产。

原有业务系统仍然保留 Spring Boot 2.2.10.RELEASE 的技术基线。

但重新查看历史后,我发现这次探索并不是完全没有留下成果。

部分兼容性修改已经提前进入生产主线。

最终实验分支保留了真实的依赖迁移和代码调整。

与此同时,原本就在推进的模块化和测试治理继续发展,项目内部的代码归属、依赖方向和验证方式也在逐步调整。

从技术角度看,这次经历让我更清楚地认识到:

框架升级不仅是修改 Maven 版本号,还需要理解依赖版本之间的关系,以及实际验证的范围。

这次复盘中的 Nacos 和 Elasticsearch 就是很好的例子。

最开始,我通过 POM 和 BOM 判断版本关系。

后来,实际执行 Maven Dependency Tree 才确认:

  • Nacos Client 从 1.3.3 变成了 1.4.1。
  • Spring Data Elasticsearch 从 3.2.10.RELEASE 变成了 4.2.11。
  • Elasticsearch REST Client 仍然保持在 7.6.2。

这些结果不仅纠正了此前部分不准确的版本判断,也让后续调查有了更明确的技术方向。

但版本组合存在潜在兼容性风险,与当年实际发生过某个确定故障,并不是一回事。

从工程管理角度看,我也重新认识了 Git 的局限。

代码版本管理与实验过程记录,不能完全互相替代。

Git 可以告诉我最终保留了哪些修改,却不一定能解释每一次实验为什么开始、如何调整,以及为什么停止。

而从这次写作过程来看,我又获得了另一项认识。

最初,我并没有计划专门写一篇 Spring Boot 升级复盘。

只是整理 Wiki 和 Git 历史时,AI 提醒我,这段没有最终上线的经历也可以记录下来。

随后,AI 不断提出问题,我逐渐回忆起当时的开发习惯,再通过 Git 历史和 Maven 依赖解析验证、修正这些认识。

最终,我们并没有完整恢复当年的每一个实验步骤,也没有找到所有异常的根本原因。

但这并不妨碍我把目前能够确认的技术事实整理清楚。

能够证明的地方,就明确记录;有依据但无法完全确认的地方,就谨慎推断;缺少证据的地方,不强行补全。

我认为,这比为了让一篇技术文章看起来完整,而重新构造一段并不存在的故障排查过程更有意义。

如果以后重新开展类似的技术实验,我仍然会采用小步开发、独立验证、尽早集成的方式。

但我会尽量保留实验基线、问题现象、验证结果以及每次调整的原因。

AI 可以帮助我完成这些记录,也可以通过提问和调查,帮助我发现那些过去没有意识到值得记录的工程细节。

这就是这次未完成的 Spring Boot 升级,最终留给我的技术记录与工程认识。


附录一:关键 Git 提交记录 ​

以下是本次复盘中使用的部分关键提交。

本表中的 Git 提交编号均已脱敏,仅用于关联正文中的技术改造与历史分析,不代表真实 Git Hash。

由于历史可能经过 cherry-pick、rebase 或重新应用补丁,提交日期不一定等于对应修改首次完成或实际验证的日期。

脱敏编号可以确认的内容
BASE-01V14 核心升级补丁所基于的生产代码
UPG-01Spring Boot 2.5.14、Spring Cloud 2020.0.6、LoadBalancer 及 Console CORS/Security 等修改
UPG-02Nacos Discovery 依赖整理
UPG-03Elasticsearch 实体与索引相关调整
UPG-04Portal CORS 配置调整
FIX-01Feign 参数名称显式化,相关修改进入生产基线
FIX-02与升级准备有关的 DTO、Lombok 调整
FIX-03上述 DTO 调整的后续修正
MOD-01早期 Maven 模块命名与结构整理
MOD-02公共工具模块拆分
MOD-03Mapper 相关代码迁移
MOD-04工具类及对应测试迁移
MOD-05旧公共模块从根 Maven 模块列表中移除
TEST-01JUnit 5 迁移

这些提交主要支持文章中有关代码变化和模块演进的事实。

对于当时的具体动机、测试程度、分支重建原因和实验暂停过程,则仍需结合个人回忆判断。


附录二:Spring Boot 升级前后的依赖版本记录 ​

为了方便以后重新调查或复现这次 Spring Boot 升级实验,我根据保留的 Maven POM 文件,以及本次重新执行的 Maven Dependency Tree,整理了升级前后的主要依赖版本。

本次对照使用以下依据:

  • 当前生产分支:Spring Boot 2.2.10.RELEASE 对应的 Maven POM 和 Dependency Tree。
  • 升级实验分支:Spring Boot 2.5.14 的 V14 分支,对应的 Maven POM 和 Dependency Tree。
  • 检查日期:2026 年 10 月 15 日。

需要说明的是,当前生产分支来自后续持续维护的代码,已经经历了部分内部模块治理。因此,两个分支的 Maven 模块结构存在差异。

本次对照重点是框架和第三方依赖版本,而不是将两个分支全部代码结构差异视为升级带来的变化。

1. 核心框架版本 ​

组件当前生产分支V14 实验分支
Java88
Spring Boot2.2.10.RELEASE2.5.14
Spring Cloud BOMHoxton.SR122020.0.6
Spring Cloud Alibaba BOM未单独引入2021.1
Apache Dubbo2.7.82.7.8
MapStruct1.3.1.Final1.4.1.Final
负载均衡组件Ribbon StarterLoadBalancer Starter

本次实验保持 Java 8 和 Dubbo 2.7.8 不变,主要调整 Spring Boot、Spring Cloud 及相关依赖组合。

2. Nacos 实际依赖版本 ​

以下版本来自两个分支的 Maven Dependency Tree。

组件当前生产分支V14 实验分支
Nacos Discovery Starter2.2.3.RELEASE2021.1
Nacos Client1.3.31.4.1
Nacos Common1.3.31.4.1
Nacos API1.3.31.4.1

因此,Nacos Client 的实际依赖版本变化为:

1.3.3 → 1.4.1

本次版本对照以 Maven Dependency Tree 的实际解析结果为准。

此前在 POM 中出现过未影响最终依赖解析的版本属性配置,但它不代表实际加载的客户端版本,因此本文不再单独展开。

3. Spring Data Elasticsearch 与客户端版本 ​

以下是本次复盘中最值得关注的一组依赖变化。

组件当前生产分支V14 实验分支
Spring Boot Data ES Starter2.2.10.RELEASE2.5.14
Spring Data Elasticsearch3.2.10.RELEASE4.2.11
Elasticsearch REST High Level Client7.6.27.6.2
Elasticsearch REST Client7.6.27.6.2
Elasticsearch Core7.6.27.6.2
Elasticsearch Transport Client7.6.2本次依赖树未出现

Spring Boot 默认依赖管理的版本与项目实际使用的版本还存在区别:

组件Spring Boot 2.2.10 默认值Spring Boot 2.5.14 默认值
Spring Data Elasticsearch3.2.10.RELEASE4.2.11
Elasticsearch REST High Level Client6.8.127.12.1

但项目根 POM 在两个分支中均保留:

xml
<elasticsearch.version>7.6.2</elasticsearch.version>

本次 Dependency Tree 已确认,这一版本最终体现在实际解析出的 Elasticsearch REST 客户端依赖中。

因此,本次实验的实际组合为:

text
当前生产分支:
Spring Data Elasticsearch 3.2.10.RELEASE
Elasticsearch REST Client 7.6.2

V14:
Spring Data Elasticsearch 4.2.11
Elasticsearch REST Client 7.6.2

Spring Data Elasticsearch 官方兼容性表中,4.2.x 对应 Elasticsearch 7.12.0。

V14 实际使用的客户端版本低于这个对应版本,因此有必要关注兼容性问题。

不过,仅凭版本差异不能直接判断当时发生了什么故障。

4. 其他主要第三方依赖 ​

除核心框架及部分相关组件外,实验分支还保留了多项原有依赖版本。

组件当前生产分支V14 实验分支
MyBatis-Plus BOM3.5.153.5.15
P6Spy3.9.13.9.1
Springfox Swagger2.9.22.9.2
Fastjson1.2.701.2.70
Hutool5.6.55.6.5
OkHttp3.14.93.14.9
AWS Java SDK BOM1.11.10001.11.1000
Apache Commons CSV1.9.01.9.0

这些数据主要来自新旧 POM 的声明或依赖管理配置,不代表已经逐一重新解析上述全部第三方组件的传递依赖。

5. 外部运行环境 ​

部分组件不直接通过项目 POM 管理,但它们的版本同样可能影响兼容性分析。

组件版本说明
JDKJava 8本次实验未升级 Java 运行时
Nacos Server2.0.3当时记录的独立服务端版本
Elasticsearch Server7.6.2本次复盘核实,升级实验时版本待确认
数据库服务端未在本次对照中核实以实际部署环境为准

其中,Nacos Server 与 Nacos Client 需要分开记录。

本次实验涉及 Nacos 客户端依赖调整,但没有证据表明同时升级了 Nacos Server。

6. 如何理解 POM、BOM 与 Dependency Tree? ​

这次复盘中,我最初主要通过 POM 属性和 BOM 版本判断依赖情况。

后来补充 Maven Dependency Tree,才得到了部分组件更加明确的版本结果。

因此,需要区分三种情况:

  1. 显式声明版本:POM 直接指定具体版本号。
  2. BOM 管理版本:由 Spring Boot、Spring Cloud 或其他 BOM 提供依赖管理。
  3. 实际解析版本:Maven 在当前项目、模块和依赖关系下最终选择的版本。

例如,Spring Boot 2.5.14 默认管理的 Elasticsearch REST High Level Client 是 7.12.1。

但 V14 最终解析出的版本为 7.6.2。

项目继承了 spring-boot-starter-parent,通过 elasticsearch.version 属性覆盖了继承体系中的默认值。

Nacos 的情况则提醒我,在 POM 中写入一个 Properties 版本属性,并不意味着它就会自动改变某个第三方依赖的最终版本。

只有属性真正被相应依赖声明或有效的版本管理配置使用时,它才会参与版本决定。

本次复盘中,实际的 Nacos Client 版本仍以 Maven Dependency Tree 为准。

此外,本次第一次检查 V14 时,依赖树中出现了后来模块治理产生的内部模块。

由于这些内部模块采用固定版本号,而不是 SNAPSHOT 版本,我怀疑 Maven 当时复用了本地仓库中来自其他分支的同版本构件。

随后使用 -am 让相关 Reactor 模块参与解析,才取得与 V14 源码结构一致的依赖路径。

这个经历也说明,调查历史分支时,除了核对 POM,还需要注意 Maven 是否实际使用了当前分支的内部模块。

当然,Dependency Tree 只能证明当前构建模型的依赖解析结果,不能自动证明这些组件在历史运行环境中完成了哪些测试。

如果以后需要更精确地复现某个历史版本,还应保存对应 Git 基线、Maven 配置、依赖解析结果及必要的运行记录。


附录三:官方技术资料 ​

以下资料用于核对本文涉及的框架行为及版本对应关系。

  1. Spring Framework 5.3:CorsConfiguration API
  2. Spring Boot 2.2.10:Dependency Versions
  3. Spring Boot 2.5.14:Dependency Versions
  4. Spring Cloud Alibaba:版本说明
  5. Nacos 2.0:兼容性说明
  6. Spring Data Elasticsearch:3.2 到 4.0 迁移指南
  7. Spring Data Elasticsearch:4.0 到 4.1 迁移指南
  8. Spring Data Elasticsearch:4.1 到 4.2 迁移指南
  9. Spring Data Elasticsearch:版本对应关系
  10. Spring Boot 3.0:迁移指南
  11. Git:cherry-pick 官方文档

这些官方资料能够解释框架版本之间的技术差异,但不能替代当时实际运行环境的异常日志和测试结果。