内容概要
Vicente Soriano 和 Raquel Pau 探讨了在 Java 应用中升级 Spring 依赖的复杂挑战,特别是当处理内部通用组件或传递性依赖时。他们解释了为何不能仅满足于简单的版本升级,而是需要解决破坏性变更 (breaking changes)、弃用项 (deprecations) 和不断演进的最佳实践 (best practices),并强调了使用 Open Rewrite 这类工具来自动化这些流程的必要性。演讲还深入探讨了涉及多个相互关联的项目和团队的高级场景,强调了为有效管理依赖并避免漏洞,制定一个精心协调的升级计划至关重要。
目录
-
开场与讲者介绍
-
演讲流程与议程
-
依赖管理基础
-
为什么要讨论升级?漏洞与“别碰它”的心态
-
定义 Spring 项目
-
升级 Spring 项目的挑战
-
寻找正确的版本:Spring Boot 依赖
-
复杂的依赖链与版本鸿沟
-
升级类型及所需工作量
-
持续升级的文化:自动化工具
-
主版本升级:破坏性变更、弃用项与最佳实践
-
演示 1:次版本升级(从 Spring Boot 3.3 到 3.4)
-
使用工具处理破坏性变更:Open Rewrite
-
Open Rewrite 的优势
-
演示 2:使用 Open Rewrite 进行主版本升级(从 Spring Boot 2.7 到 3.0)
-
最坏的情况:复杂的升级编排
-
多项目主版本升级的挑战
-
具体案例:升级 Acme 平台
-
升级计划
-
在复杂场景中使用 Open Rewrite 的问题
-
对 Open Rewrite 配方 (Recipes) 的建议
-
介绍 Tanzu Application Advisor
-
演示 3:使用 Tanzu Application Advisor 应对复杂升级计划
-
总结与问答
开场与讲者介绍
Vicente Soriano: 好的,非常感谢大家来参加这次演讲。首先提醒一下,以防你们不知道,同一时间还有 Alex Soto 和 Rosen Stoyanov 的演讲。
万一你们走错了房间呢?没有吗?好的。那这次演讲将会是最好的。欢迎来到我们的主题分享:持续对齐和升级 Spring 依赖的生存指南。
我们先做个自我介绍。我叫 Vicente Soriano,这是我的照片。我是 Broadcom 的一名软件工程师,主要在 Spring 商业团队工作。
昨天,Daniel 在他的演讲中完美地描述了我们的工作:我们基于现有的 Spring 框架和整个生态系统来构建产品,并交付给我们的客户。
Raquel Pau: 大家好,我是 Raquel,也在 Broadcom 工作。但我不是软件工程师,而是一位产品经理,不过我有很强的工程背景。我的专业领域是编译器以及与执行代码变更相关的工具。
希望大家喜欢这次演讲。
演讲流程与议程
Raquel Pau: 首先,我想确保大家了解我们这次演讲的流程,因为可能一开始你们会觉得内容太基础了。
然后就想离开。不,请一定留下来,因为最精彩的部分在最后。首先,我们会介绍一些关于依赖管理 (dependency management) 的基础知识,可能这些你们都已经知道了。
接着,我们会做一些演示,展示在 Spring 环境下进行次版本 (minor) 和主版本 (major) 升级意味着什么。然后就是最精彩的部分:升级编排 (upgrade orchestration)。
我们会探讨那些复杂的场景,也就是非理想路径 (non-happy path),比如当你有多个项目需要升级,并且需要大量团队协调时该怎么办。所以请务必坚持到最后。
依赖管理基础
Vicente Soriano: 好的,那我就先负责讲前面比较枯燥的部分。我们想从头开始,可能大多数人都知道什么是依赖 (dependency)。
这是 Google 给出的一个定义。现在大家都在用 AI,所以我想,为什么不在这次演讲中也用一下呢?这个定义基本上是这样:
这是一个很好的定义,它描述了软件组件之间的一种关系,其中一个组件依赖于另一个组件才能正常运作。但我更喜欢从另一个角度思考,我们日常工作中也常常这样想:
我们使用的每一个依赖,无非就是一个导入的库,它可以让我们不必重新实现那些已经存在的功能。
既然已经有人做好了,我们就不需要再造轮子了。这里有一个例子,不好意思用了一个不太出名的依赖。这个依赖来自 Spring Boot。
它为实现测试等功能提供了很多便利。这是一个非常知名的依赖。当我们讨论依赖时,实际上我们是在利用他人已经完成的工作,从中获益匪浅。
整个社区,基本上整个开源世界都在为此做贡献。正如我们之前所描述的,我们不是在重新发明轮子。
我们正受益于这些成果。当然,这也有一个弊端或缺点。当我们使用这些依赖时,最终会基于这些构建模块 (building blocks) 来构建应用程序、系统和基础设施。
结果可能就是,我们最终用到了某个由内布拉斯加州的某个人从 2003 年开始维护的依赖。我不是说这一定危险,但确实是。
我不知道,这确实是一个有点复杂的情况。
为什么要讨论升级?漏洞与“别碰它”的心态
Vicente Soriano: 那么,我们为什么要讨论升级呢?主要就是因为第二个话题:漏洞 (Vulnerabilities)。大家都知道漏洞是什么。
我们希望尽一切可能避免它们。CVE 是这样描述漏洞的。对我们来说,我不知道这意味着什么,但很明显,我们不希望自己的应用程序里有漏洞。
我们不想把它们交付给客户,也绝不希望我们的依赖里存在漏洞。长期以来,很多人都信奉一个信条,谢谢。我不会说是公司,但也许是。那就是“如果它能用,就别碰它”。
我们不再遵循这种做法了。我认为在软件开发领域,这种想法早就过时了。
我的意思是,甚至不是最近,而是从来就不该有这种想法。所以我们不会这样做。但我想提醒大家,曾经有一段时间,人们热衷于使用最新的技术。
但最终的答复总是:没有资源来做改变,我们不打算升级,现在这个系统运行得很好,还在赚钱,请不要动它。
我们应该专注于商业价值。但这种情况最近,就在去年,发生了改变,而原因只有一个:安全 (security)。这很有趣,不是吗?
至少对我来说,这引发了我的思考。我想分享一个个人经历。几年前,我开发了一个工具,用来进行代码变更。
我当时试图自动化这些变更。那个工具叫 Walmart,也在好几个会议上展示过。但在那个时候,人们对执行这些升级并不感兴趣。
原因很简单,因为没有动力。升级只会带来风险。那么为什么要这么做呢?现在情况完全不同了。
是的,没错。举个例子,一个不久前发生的案例,虽然已经过去几年了,但可能大家还记忆犹新。
不知道有没有人听说过 Log4Shell 漏洞。那一次,它简直让世界天翻地覆。我们一到办公室就开始给所有东西打补丁、做升级,就因为某个愚蠢的日志库被别人成功利用了。
定义 Spring 项目
Vicente Soriano: 好了,最后一个话题,抱歉内容有些枯燥。我们想定义一下我们所说的项目 (projects)。
这里我们指的是 Spring 项目,你们很快就会明白为什么。基本上,我们将这个解决方案和方法的核心放在 Spring 项目上,尽管它也适用于开源生态系统中的任何其他类型的项目。
当你访问 spring.io 网站,想查找某个项目的信息以便在你的应用中使用时,它就是一个项目。
网站上已经有相关描述。我们在这里想强调的是,我们会频繁使用“项目”这个词。
我们想澄清一下,为什么我们认为升级应用程序的方法不应该只关注单个依赖,而应该着眼于整个项目。
项目基本上就是 Maven 的构件 (artifacts),任何发布在公共仓库或内部仓库中可供使用的东西。它们通常共享一个目标或使命。
它们共享同一个发布仓库,功能相同,而且通常,这一点很重要,它们遵循一个发布节奏 (release cadence),即一系列版本,其中每个依赖都遵循相同的命名规则。
我们说它是一系列构件的集合,主要原因在于,最终很多项目,如果不是大多数的话,都采用多模块 (multi-model) 的项目结构。
它们是多模块的 Maven 或 Gradle 项目。因此,所有这些构件通常会同时发布,并且版本号完全一致。
这个模式在 Spring 中很常见,我们也看到许多其他开源项目也在遵循。
但最终,我们想关注的是项目层面,因为重要的不是某个特定依赖的变化。升级的关键在于某个特定的流程,因为所有已发布的构件之间存在着相互关系。
这就是原因所在,也是为什么某些工具可能会缺少我们稍后会看到的一些功能。
升级 Spring 项目的挑战
Vicente Soriano: 是的。这是一个基础的例子。你访问 start.spring.io,想为你的应用添加 Spring Security。
你在这里选择的是项目,尽管它会添加几个依赖。你可以看到,这些依赖属于两个不同的项目。前两个属于 Spring Boot。
最后一个属于 Spring Security。它们的版本管理方式不同。但基本思路就是这样。那么,正如 Raquel 刚才所说,我们为什么关注项目呢?
因为你在你的应用中使用的是项目。你可能正在使用最基础的 Spring Boot 例子,版本是 2.7。你需要考虑升级。
你可能会想,好吧,在升级过程中的某个阶段,我至少要达到其中一个版本,因为我正在升级,希望能升到最新版。抱歉没写 3.5,它已经发布了。
但我做幻灯片的时候还没发布。这是一个简单的例子,对吧?我只需要提升一下版本号,可能再修改一两处地方,就完成了。
但通常情况并非如此。真实情况是,你引入 Spring Boot 是因为你的应用中还用到了其他东西。
比如,从 Spring Security 调用的 API,或者通过 Spring Data JPA 从内部平台访问数据库等等。
这些依赖本身还有更底层的依赖,即使你没有直接使用它们,比如 Spring Framework 和 Reactor。
然后你开始发现版本之间存在差距。比如,没有与 Spring Boot 3.1 或 Data JPA 3.1 相关的版本。
如果你在使用 Webflux,会发生什么呢?在这种情况下,问题不大,因为 Spring Boot、Spring Security 等版本仍然可以与 Spring Framework 6.0.0 配合使用。
但这是因为它是 Spring Framework。如果不是呢?你就需要把这一点考虑进去。
Raquel Pau: 我有个问题。我看到这里有来自不同项目的不同版本。我怎么知道应该用哪个正确的版本呢?尤其是在我正在构建一个 Spring Framework 应用的情况下。
我该如何选择?
寻找正确的版本:Spring Boot 依赖
Vicente Soriano: 问得好,Raquel。谢谢。下一个问题。开个玩笑。是的,在 Spring 应用和项目的生态系统中,我们有这样的机制。
Spring Boot 通常是所有 Spring 应用的核心项目。你可能在使用其他各种项目,这没关系。
但 Spring Boot 的一个优点是,它在自己的仓库里发布了一个构件。我想我这里有链接。是这个,抱歉。这个构件叫做 Spring Boot Dependencies。
这是一个非常有用的构件,主要是因为当你使用 Spring Boot starter parent 时,它会利用这个构件来统一管理许多其他项目的版本,而你甚至不会察觉到。
如果你想确切知道,当你使用 3.5 版本时,应该使用哪个版本的 Reactor,或者在升级时应该使用哪个版本,你可以在这里查询。
这里基本上是 Maven 中央仓库,由 Sonatype 托管。你可以在这里看到所有可能用到的其他项目的版本号,主要是 Hibernate,Reactor 可能在下面。
Raquel Pau: 我想在这里分享一点。大家知道,Spring 项目有成百上千个,每个项目可能由不同的人维护。
因此,不同项目之间可能会存在一些不一致,尤其是在第三方开源依赖方面。最终的问题是,到底哪个版本组合才是正确的,才能确保所有组件能协同工作?
答案就在这个 Spring Boot Dependencies 文件里。如果你需要自己制定升级计划,Spring Boot Dependencies 是一个非常好的参考。
另外还有 Spring Cloud Dependencies,作用类似,但针对的是 Spring Cloud 产品。Spring Data 和其他一些项目也有自己的依赖管理文件,因为它们有自己独立的发布节奏。
所以,这是一个非常有用的资源,特别是在你构建 Spring Framework 应用时。
Spring Boot 通过父依赖已经帮你解决了很多问题。但如果你在构建一个纯粹的 Spring Framework 应用,你就失去了这种便利。这时候,这个文件就是你判断版本兼容性的“真理之源”。
复杂的依赖链与版本鸿沟
Vicente Soriano: 没错,这说得很好。正如你所看到的,我新增了一行,是 Hibernate。这可能是我应用中需要更新的另一个新依赖。
如果我遵循 Spring Boot 或者 Spring Data 的发布节奏(因为它可能是那个项目使用的子依赖之一),你会看到在 3.1 和 3.2 版本之间有一个空缺,跳过了 6.3 版本。
为什么会这样?主要是因为他们在发布那个版本时,选择了特定版本的 Hibernate。这是否意味着如果我只升级 Hibernate 到 6.3 版本,它也能正常工作?
有可能,但我们不确定。如果你去查 Spring Boot Dependencies 文件,你还是不确定,因为它告诉你,如果你要用 3.2 版本,就应该用 6.4 版本的 Hibernate。
这是他们测试过的版本,也是他们以建议的形式提供给社区的。但这还没完,事情会变得更复杂。
我们又加了一个新的依赖,但这个依赖并不在 Spring Boot 的管理之下,而是它自己依赖于 Spring Boot,并且有自己的发布节奏。
版本对齐完全乱了。现在我们有个 3.3 版本,跟其他任何东西都对不上。如果我把 Spring Boot 从 3.3 升级到 3.4,Java CFM 还能用吗?
我需要跳过某个 Java CFM 的版本吗?这变得非常困难,而且还不止于此。可能还有更多这样的依赖,而所有这些都是开源项目。
我们还没算上你自己的内部组件或项目。所以,你可以看到,要制定一个合理的升级计划是相当困难的。
升级类型及所需工作量
Vicente Soriano: 是的,升级你的应用。这里我们描述的是,通常情况下,升级只涉及到提升你正在使用的依赖项目的版本号。
我们在这里列出了几种情况:如果只是因为修复了一些 bug 或解决了一些 CVE 漏洞而升级到一个新的补丁 (patch) 版本,通常工作量很小。
只要把新的补丁版本号填上,它就能工作。你也可以升级到一个次版本 (minor)。这个工作量就稍微大一些,因为根据你升级的项目不同,可能会引入破坏性变更 (breaking changes)。
不是每个项目都遵循语义化版本 (semantic versioning),你可能并不清楚。
Raquel Pau: 那有什么好办法能知道是否有新版本可用呢?因为毕竟,补丁版本可能是为了修复 bug,也可能是为了修复某个特定的漏洞。
那么,有什么建议能让我们的应用保持最新状态呢?
持续升级的文化:自动化工具
Vicente Soriano: 是的。我不知道,可能大多数人已经知道了,但要保持升级、形成持续升级的文化,通常的做法是使用自动化工具,比如 Dependabot 或 Renovate。
你们中的大多数人可能已经在你们的CI/CD 流程中集成了这些工具。这通常是很好的做法,即使它不能解决所有问题,至少版本号的提升是由它来处理的。
Raquel Pau: 我还想在这里强调或者问一下,你们知道这些工具是否了解你需要管理的依赖版本之间的相互关系吗?
我认为它们并不知道这种相互关系,这也是为什么升级某些应用如此困难的原因。
Vicente Soriano: 说得对。
主版本升级:破坏性变更、弃用项与最佳实践
Vicente Soriano: 是的,对于更新的主版本 (major versions),通常需要改变主版本号。耶!但这可能是你要做的最费力的工作,因为可能会有破坏性变更等等,这就是我们接下来要谈的。
你通常不只是改个数字,然后说“嘿,我升级了我的应用”。不,你很可能需要处理那些破坏性变更。也就是版本之间发生变化的东西。
可能是方法名、类型变了,也可能是参数或参数列表变了。嗯,还有注解也可能变了。
基本上什么都可能变。这些都是让升级变得复杂的原因。还有弃用项 (deprecations),也就是那些不再使用的东西。
对于这些,你可能除了去查看发布说明 (release notes),没有别的办法。说明里会告诉你,某个东西不应该再用了,因为我们不再提供支持,你应该改用另一个东西。
这也是你在升级应用时通常需要做的一项重要改动。最后,但同样重要的是,最佳实践 (best practices) 的变化。
当我们使用任何类型的依赖或项目时,我们都想遵循最佳实践。这些通常是项目作者的建议,或者是行业内某种事实上的标准。
这种情况从 Java 最早的版本就有了。比如 Java 8 引入了流 (streams) 和 Optional,为处理空值等问题提供了新的、更好的方法。
至少在处理这些问题上是这样。所以,这些都属于升级应用时需要做的工作。
演示 1:次版本升级(从 Spring Boot 3.3 到 3.4)
Vicente Soriano: 现在是第一个演示。这个会非常简单,你们会看到……
Raquel Pau: 是的。这个演示的目的,基本上是想向大家展示,将一个 Spring 应用升级到下一个次版本意味着什么。
我们已经看到,次版本升级可能需要也可能不需要代码改动。因为至少在 Spring 的世界里,当我们发布一个次版本时,通常意味着某些方法被标记为弃用 (deprecated),因此这是一个使用新 API 的好机会,而不是把这些技术债一直拖到下一个主版本。
Vicente Soriano: 好的。基本上,我访问了 start.spring.io,下载了一个应用,然后修改了所使用的 Spring Boot starter 的版本。
我引入了几个 Spring Web 的依赖,所以这是一个 Web 应用。它们都属于 Spring Boot,但我会使用 Spring Web,所以 starter-web 也在里面。
我用的是 3.3 版本,这已经不是最新的了,相当过时,所以我们需要更新它,对吧?我们需要升级。我们来看看这个项目本身。
这是项目的结构。我们有一个 Spring Boot 应用,没什么新东西。这里定义了一个控制器。
基本上,这个控制器模拟了快递员的工作,就是递送一个白金芯片。这是《辐射:新维加斯》里的东西。
Vicente Soriano: 放大一点,抱歉。
Raquel Pau: 哦,好的。
Vicente Soriano: 抱歉。这样可以吗?好的,谢谢。它基本上就是返回一个字符串,没什么特别的。
我们为测试写了一些实现。
Vicente Soriano: 我再放大一点。好了。这是一个集成测试,用来检查控制器是否能正确响应,是否返回了预期的字符串。
我要运行这个测试,希望能成功,因为这是我的应用,这是我应用当前的状态。
现在要把它升级到下一个次版本。这很简单。但同样,我是在为我的应用做升级。这样可以吗?
我想可以。那我们到这里来,输入 mvn package test,执行所有操作。希望它能下载好依赖,然后……失败了。太好了。
Vicente Soriano: 是不是……抱歉。观众真给力,谢谢。是的,如果我弄错了,就会这样。好了,我们可以看到,Spring Boot 应用已经初始化,并且已经是我想要的版本了,测试也通过了。
几乎一切都正常。这就是升级应用的方法。非常感谢。不,开玩笑的,后面还有内容。
Raquel Pau: 我想在这里强调一点,这是 Spring 发布新版本的方式。但也有其他开源项目,其中一些也包含在 Spring 的依赖中。
我想说,因为它们可能还处于起步阶段,所以在次版本中也会引入破坏性变更。这种情况你们可能会遇到。
所以,Spring 是这样发布新版本的,但这可能不适用于其他开源项目。这只是……是的。
使用工具处理破坏性变更:Open Rewrite
Vicente Soriano: 我们接着往下讲。我们已经讨论了破坏性变更。那如果我需要修改的东西已经不存在了,该怎么办?
在我提升版本号之后,有些东西变了,单纯的版本提升已经不够了。我们想在这里介绍一个工具,昨天参加了 AssertJ 演讲的同学可能听 Tim 提到过。
虽然那次演讲不是关于这个工具的,但他提到了。这是一个非常好的工具。我们需要一个能简化这个过程的工具,帮助我们处理所有必要的变更,而不是自己去翻发布说明,然后手动修改。
可能不是改一行代码,而是 100 行,甚至可能影响到我们整个项目组合中的不同项目。我们不知道。所以我们需要一个工具。
我们在这里介绍的“二号玩家”,就是 Open Rewrite。这里有谁……是的,没错。
有谁已经在使用 Open Rewrite 或者听说过它?请举手。我觉得,我们换个方式问。
这里有谁从来没听说过或者没用过 Open Rewrite?很好。好的。那你们就把它当作一个重要的收获带回去。就这样。不,这是一个非常棒的工具,功能非常强大。
Open Rewrite 的优势
Vicente Soriano: 它能帮你完成升级,至少能帮你处理掉大部分需要做的工作。我们在这里列出了它的一些优点。
首先,它能精确地进行重构。它使用的技术主要是将你的应用加载成一个所谓的无损语义树 (lossless semantic tree),这是你代码的一种表示形式,但保留了所有的类型信息。
如果我说错了请纠正,包括类型、名称等。希望你们可能已经用过 Checkstyle、PMD 这类静态代码分析工具。
Raquel Pau: 主要区别在于,那些静态代码分析工具基本上只是在解析源代码,也就是文本表示。
而 Open Rewrite 不仅仅是解析源代码,它还利用了编译器内部的 API 来解析每一个表达式的类型。
因此,它能让你通过编程的方式来定义你想要如何改变你的软件。
Vicente Soriano: 并且能精确地做到这一点。
Raquel Pau: 确实。如果你还记得,在执行 Checkstyle、PMD 或者 SonarQube 时,都有可能出现误报 (false positives)。
主要原因就是它们没有类型推断 (type inference) 的功能。而使用 Open Rewrite,你可以编写自己的重构规则,体验就像在 IDE 里做重构一样。
另一个相关的特性是,当你修改源码时,这并不是什么很旧的技术了,通常工具只是在内存中应用这些变更,然后按照特定的风格重新格式化代码。
它们会在之后使用一个格式化工具,这样就很难看清楚原始的变更是什么,因为整个文件都被重新格式化了。
而 Open Rewrite 能理解文本的哪些部分需要被修改,因此它不会重新格式化整个文件,这样就很容易审查某个特定变更的影响。这种变更通常被称为配方 (recipe)。
Vicente Soriano: 是的,没错。换句话说,任何会改变你代码缩进的工具,基本上都是在给你添乱。
你不需要那样的工具。你需要的是一个能帮你升级,但又能保持代码原有风格的工具,因为你就喜欢那样。这就是第二点。
你可以看到,Raquel 提到了配方 (recipes)。配方是 Open Rewrite 用来应用变更的概念。
基本上,你可以创建原子化的配方来处理一些小的变更,比如方法名变了,或者某个类的构造函数参数变了等等。
你还可以把这些配方组合起来,创建更复杂的配方。他们提供了工具让你自己编写配方,如果你正在为自己的组织开发库的话。
他们还提供了大约 3000 个配方,涵盖了许多开源项目。
你需要检查一下你的项目是否在其中。另外,一个有趣的事实是,这个工具诞生于 Netflix,当时他们需要对日志库进行重构。
如果我没记错的话,他们需要迁移到 Log4j。所以这个工具诞生于 Netflix。现在这个项目由 Modern 公司所有。你可以去他们的展台看看。
这个项目的作者已经转移了,现在它完全由 Modern 公司拥有。
如果你想了解更多信息,可以去他们的展台,他们能提供更详细、更准确的信息。我们用过这个工具,它真的很棒,强烈推荐。
最后一点,它支持 Maven 和 Gradle,所以可以应用到你的 Maven 或 Gradle 项目中。
Raquel Pau: 是的。Open Rewrite 本身不是一个命令行工具。你应该通过 Maven 或 Gradle 来执行它。主要原因在于,Open Rewrite 需要加载你的类路径 (classpath) 才能理解类型信息。
而 Maven 能提供这些信息。
演示 2:使用 Open Rewrite 进行主版本升级(从 Spring Boot 2.7 到 3.0)
Vicente Soriano: 好的,我们来看第二个演示。我简单解释一下我们要做什么。基本上和之前差不多。
我从 start.spring.io 下载了另一个新的应用,也引入了 Spring Web 来创建几个控制器。
我们来看一下 pom.xml。这是主要的内部结构。我修改了要使用的 Spring Boot 依赖的版本,准备用 2.7 版本。
这个版本比我们之前展示的还要旧。你可以看到这里的依赖和之前一样。这次的演示我叫它“Green Fandango”。
不知道有没有人玩过这款游戏,强烈推荐。我们有一个 Spring 应用。我来展示一下我用来表示客户的 record,抱歉。
我可以把它应用到每个窗口吗?好的,我想可以。这正好和放大相反。我们有一个 record 来表示客户。
客户最主要的信息是,我们需要判断他是好人还是坏人。我们还有一个旅行社代理。旅行社代理基本上就是向客户销售旅游套餐。
就是这样。我创建了一个控制器,叫做“死亡部控制器”。这个控制器基本上就是向每个客户销售旅游套餐。
在我的死亡部,有一个特殊规定。如果客户是好人,
如果客户是好人,我就不卖给他任何旅游套餐,我会把套餐收回来。
然后控制器会返回一个状态码为 406 Not Acceptable 的 ResponseStatusException。否则,我们就卖出这个套餐。
这里还有另一个控制器,把两者结合起来了。我有两个集成测试来验证这个功能是否正常工作。
它只测试了功能本身。第一个测试用例尝试……然后失败了。太好了。
Vicente Soriano: 类加载器 (Class loader)。
Vicente Soriano: 尝试拒绝。我们来试试这个,因为当我们开始修改版本号时,任何被删除的东西……那是什么?
在这里吗?好的,我们把这些都删掉。
Vicente Soriano: 我不确定。
Vicente Soriano: 好的。幸好我之前录了视频。让我跳到已经执行完的部分。是的,这里我已经运行了测试。
测试结果是绿色的。抱歉,这个视频我没法放大。但我可以切换到这个界面。这是集成测试。
我主要测试的是,第一个到达的客户是个坏人,然后旅行社成功卖出了旅行套餐。没问题。欢迎你踏上死亡之地之旅。
第二个客户,情况对她来说不太好。至少她是个好人。当我们需要为她安排行程,当旅行社代理要卖给她旅游套餐时,收到了一个“不可接受”的响应。
我在这里还实现了一个额外的测试,是一个控制器的单元测试。我专门检查了当客户是好人时,返回的状态码是不是“不可接受”。
这个测试也能通过。我就不运行其他测试了,但这个测试可以正确运行我的应用。即使应用版本很旧,测试仍然适用。
是的,我在这里做的就是重新运行应用,编译它。我就在这里停下,因为剩下的应该都能正常工作。
我不知道。现在我要做的是升级应用。但为了升级,我不想像之前那样只是简单地提升版本号。
我现在想做的是,我们来看看这个能不能行,我想应用这个。这个东西,不知道大家能不能看清?
这是一个通过 Maven 插件执行 Open Rewrite 的例子。它引入了 Spring 的配方 (recipes),使用的是最新版本,然后选择了特定的升级配方,即 upgrade-spring-boot-3.0。
这是我想要达到的目标版本。我没想到又要下载东西了。
Raquel Pau: 这是因为你用的是 release 版本,可能他们发布了新版本。
Vicente Soriano: 好的,有道理。它运行了配方,做了一些事情。这里有一些信息。但可能最好的查看方式是执行 git diff,看看有什么变化。
现在我们看到,我没有动任何代码,但版本号变了,而且 getStatusCode 这个方法也变了。
如果我尝试在不修改那个方法的情况下升级到 3.0,肯定会失败,因为 getStatus 这个方法在 ResponseStatusException 类中已经被弃用了。
所以,这个工具已经帮我们升级了应用。基本上,我现在要做的就是,如果我接受这些变更,就提交到仓库,然后我的应用就升级了。
虽然还没有升级到最新版本,还需要更多工作,但这已经是升级的第一步了,对吧?
好了,接下来交给 Raquel,你们肯定听我听烦了。
最坏的情况:复杂的升级编排
Raquel Pau: 好的。我们看了一个非常基础的升级,然后是一个主版本升级。但还有比这更糟的情况吗?
有。坏消息是,通常情况会更糟。我想给大家举个例子,并引导大家一起分析。
这是你们在前面图片里已经看到的典型场景。猜猜看?Spring 通常就是这些模块之一。
Spring 依赖于其他开源项目,而你可能也在使用其他依赖于 Spring 的开源项目。同时,你还有自己的内部库,如果你喜欢 Spring,为什么不用呢?它们也用了 Spring。
这就形成了一个升级链 (chain of upgrades)。
多项目主版本升级的挑战
Raquel Pau: 这到底意味着什么呢?在我们看来,进行主版本升级时,主要有三个挑战。
首先,也是很重要的一点,需要升级和发布多个项目。你需要跨多个项目来协调升级。这很可能,我敢说 90% 的情况下,意味着多个团队需要在优先级上保持一致。
这意味着什么?大量的会议。其次,我们知道有针对 Spring 的配方,但在 Spring 之上还有很多其他项目。
如果想以正确、高效的方式做事,它们是否都应该构建自己的配方呢?你会发现,可能需要,也可能不需要。因为有时候,如果它们是上游依赖 (upstream dependencies),可能并不会引入破坏性变更。
它们可能只是升级了 Spring 或其他项目,但它们的 API 保持不变。那你为什么还要再构建一个配方呢?这值得吗?
因为我们看到,在这些通用框架或工具之上,有很多模块。如果每一个模块都需要构建一个配方,然后你还要等着那个配方完成,那将是一件非常痛苦的事情。
所以,我们会探讨这个问题。最后,但同样重要的是,有些项目就是不接受升级。
这种情况发生过。有很多重要的项目依赖于 Spring,但它们不打算发布新版本了,已经停止维护了。
在这种情况下,你就需要进行迁移。你必须进行迁移。实际上,这种情况在 Spring 内部也发生过。
在从 Spring Boot 2 升级到 Spring Boot 3 的过程中,安全部分的某些项目已经不再可用了。这是后面内容的一个剧透。
但这意味着,你需要从那些项目迁移到 Spring Security。这些都是动态的部分,你也需要进行相应的迁移。
我认为这是一个很常见的具体例子,我想和大家详细探讨一下。
具体案例:升级 Acme 平台
Raquel Pau: 好的。那么,从当前状况升级到下一个版本意味着什么呢?具体来说,我用的是 Spring Framework 5.0.3,这个版本已经不再受支持,意味着你不会再收到补丁更新,并且会暴露在漏洞风险之下。
我们的目标是重新获得支持。为了达到这个目标,我需要集成到 Framework 6.1。
然后,我有一个电商库,Acme Platform 库。大家都有自己的电商库。这个库用的是 Spring Framework。
猜猜怎么着?我们还有其他很棒的模块,比如推荐、Acme 支付,或者 Acme 设置。
其中一些用到了 Spring 的某些模块,另一些用到了其他的。我特意用 Spring Framework 来让事情变得更复杂。
好的,Acme Recommendations 用到了 Integration Security,Acme Payment Security 和 Security RSA。而且它还用到了 Acme Platform。
和 Recommendations。这意味着升级必须按照特定的顺序进行,否则依赖关系会一团糟,你甚至不知道应用最终能不能正常工作。
所以,最终的问题是,计划是什么?
升级计划
Raquel Pau: 更确切地说,升级计划是什么?我们从最佳实践开始。
我有一个平台,版本是 5.0.3,下一步是升级到 Framework 6,并且我把它升级到一个快照 (snapshot) 版本。
为什么?因为在可能的情况下,进行增量升级是一个好习惯,这样你对需要做的变更更有控制力,并且不会引入不必要的干扰。
所以,这个团队会先升级到 6.0,等他们准备好了,就会发布正式版。
目标是使用一个受支持的版本,也就是 Framework 6.1。在此期间,其他工作可以照常进行,每个人都可以正常工作。
没有人会被阻塞,因为他们使用的是固定的版本。然后,当平台准备好后,另一个团队,也就是 Acme Settings 团队(所以我用了不同的颜色),就可以开始升级了。
这次升级需要从 5.3 直接到 6.1。他们不能走中间那一步,原因和我之前解释的一样,就是缺少依赖。
现在轮到 Acme Recommendations 了。它不仅用到了 Framework,还用到了 Integration 和 Security。他们需要进行一个复合升级。
所以,我们有这样一个中间步骤,就是一个快照版本。然后我们可以继续。但猜猜怎么着?在升级到 Framework 6.1 之前,还有另一个中间步骤,这让我能更好地控制变更,那就是先升级到 Spring 6.1 和 Security 6.1。
最后,我们才能升级到 Framework 6.1,并迁移 Integration,然后发布。这样就为 Acme Payments 解除了阻塞。
现在 Acme Payments 也可以创建了,因为我的依赖都已经对齐了。你们有没有发现这里少了什么?
Acme Payment 是这个。
Raquel Pau: 别害羞。没错。这是我之前剧透的。当你升级到 Framework 6 时,RSA 已经不再可用了。
因此,这里需要进行一次迁移,因为它需要被 Spring Security 替代。很好。
这就是最终的图景。我们有四个团队,他们增量地完成了升级,最终都进入了受支持的状态。
在复杂场景中使用 Open Rewrite 的问题
Raquel Pau: 如果我运行 Open Rewrite 来做这类升级,会遇到什么问题呢?
基本上,Open Rewrite 对你的内部项目一无所知。它只会处理 Spring。
所以,如果你不注意你的依赖关系,不清楚谁应该先升级,你就会看到大量的变更。
然后你就要花时间去搞清楚为什么会这样。另外,Open Rewrite 中的大多数开源配方都遵循一个最佳实践,那就是它们包含了最佳实践的改动。
也就是说,它们包含了可选的变更。这意味着你最终会看到很多变更,也意味着很多干扰。
这不是我们真正推荐的做法。最后,嗯,看起来还不错。
Raquel Pau: 最后,让事情变得更复杂的是,Open Rewrite 并不知道你当前运行的具体版本是什么。
配方会应用所有之前版本所需的所有变更。可能你正在运行一个升级到 Framework 3.4 的配方。
而你当前的版本是 2.6,它会执行所有中间版本的变更。这意味着你会看到更多的变更,因为猜猜看?
配方,如果我用的是 Boot 3.4,第一行就是基于 3.3 的。然后它会产生一个连锁或级联的变更,涵盖所有这些版本。这通常是为什么有时候人们会抱怨,说“我们能不能把这个拆分成更小的版本?”
你还需要做的另一部分分析是,真正理解目标版本是什么,有时候这并不清楚,因为你依赖于其他外部依赖。
对 Open Rewrite 配方 (Recipes) 的建议
Raquel Pau: 从实际案例中我们学到的一点是,当你在 Open Rewrite 中创建配方时,你可以在 Spring 的配方中也看到这一点,这些配方会调用其他配方来创建它们自己的依赖。
猜猜怎么着?依赖项的提供时间与项目发布的时间并不同步。配方可能在未来的任何时候出现。这是一个普遍事实。所以,如果我是一个配方提供者,因为我有一个项目,我想让别人能升级我的 API,那么我去提交一个 PR,说“嘿,因为你用了我的依赖,所以你需要用我的配方”,这有意义吗?
不,这无法扩展。就是这样。或者,作为另一个应用开发者,你应该去搞清楚哪些配方需要一起执行吗?
这很难。所以,我们最终的建议是,配方应该只提供相应项目的 API 变更,仅此而已。
因为事实是,你需要一个引擎或者某个东西来判断哪些配方需要一起执行。所以,总结一下。
不应该包含对旧版本或依赖版本的升级,因为配方最终是在那个版本发布之后才发布的。
而需要判断哪些配方应该一起执行的那个东西,我们正在 Tanzu 中提供。
介绍 Tanzu Application Advisor
Raquel Pau: 这就是 Application Advisor。Application Advisor 会计算升级计划,也就是我们之前在幻灯片里为 Acme 手动做的那些事情。
你可以看到升级我的应用的步骤是什么,哪些依赖可以升级,需要应用的最小变更是什么。
猜猜看?如果我依赖于我项目的内部依赖,它会停下来,告诉你,嘿,你不能继续升级,直到那个团队提供了带有配方的配置,让他们先升级。
只有当那个团队提供了信息,升级才会继续。App Advisor 对 Spring 来说是最好的,因为它开箱即用地提供了更多作为企业版一部分的配方,覆盖了最重要的 Spring 项目,包括 Boot、Framework、Security、Data 和 Integration。
所以我们有一个包含数百个配方的目录,并且还在不断增长。
演示 3:使用 Tanzu Application Advisor 应对复杂升级计划
Vicente Soriano: 好的,我们有演示。我们能做吗?时间差不多了,但如果可以快速看一下 demo 3 文件夹。
我可以快速演示一下。在这个例子中,我们有一个叫做 ACME Boot Starter 的东西,它基本上是一个组件。
我来列出这里面的东西。它用的是最新版本。但如果我检查一下它的仓库,不是这个。
这个组件的仓库是这个。我看到我们已经发布了几个版本了。
从 1.0 到 6.0。需要说明的是,1.0 版本开始时用的是 Spring Boot 2.7,最新版用的是 3.4。
所以这个组件基本上遵循了和 Spring Boot 相同的发布节奏。如果我回到我的主应用,这次是 Sprint Clinic,抱歉没用游戏做例子。
我可以看到,我的 pom.xml 里,应用的版本是 2.7。如果我去看依赖列表,这里,我可以添加一个依赖。
可能在 IntelliJ 里做这个更好。不管怎样,这个 Pet Clinic 的 pom.xml 在这里。
我可以输入 group ID,是 com.acme.boot。artifact ID 我不记得了,需要查一下。
Raquel Pau: 我觉得这是一个很好的机会来解释一下这个。基本上,我们需要从那个文件里提取版本信息。
我们需要解释一下。这里有一个文件,是 Demo 3。这是 Advisor 做的另一件事,就是为这个特定项目创建一个版本映射。
它会检索信息和元信息,比如我需要使用的坐标。这个就是我想要的,仓库信息等等,还有一个版本映射,一些基本信息,比如哪个版本的构件存在,它演进到哪个版本,或者升级到哪个版本。
以及是否有任何支持的代际,也就是这个特定版本与其他项目的哪个版本相关联。这就是我们用来做版本对齐,或者至少为我们之前在表格里看到的对齐提供一些信息的方法。
Vicente Soriano: 好的,我到这里,输入这个 artifact ID,然后告诉这个应用要用 1.0.0 版本。
就是这样。我为它添加了一个依赖。最后一件事,但我现在还不会做。
抱歉,Pet Clinic。我要用 Advisor。我们看看它是否可用。在路径里,对吧?
好的。这就是我们刚才做的命令行工具。我们可以输入 advisor build config。如你所见,第一个命令会从应用中提取升级所需的信息,也可能用于提供建议等其他用途。
所以这可能是你要做的第一件事。我已经做过了。然后是 upgrade plan get。
Vicente Soriano: 我不知道。这太棒了。我有个视频。
Vicente Soriano: 好了,就是这个。这就是我想展示的。当你执行 upgrade plan get 时,它会报错。
它会告诉你,嘿,Framework、Boot、Data Commons 都有可用的升级,但有一个组件阻塞了它们。我没法帮你升级。
所以,你接下来要做的就是,把我从我的组件中提取的那个文件,通过一个环境变量提供给它,像这样:SPRING_ADVISOR_MAPPING_CUSTOM,然后如果需要的话,再提供一系列配置。
我基本上就是把它导出一个变量,以便在运行命令时可用。然后我再执行刚才输入的同一个命令。
基本上是一样的,只是加了一个环境变量。当我执行它时,我得到了一个稍微不同的结果,也就是我们整个演讲一直在描述的东西。
一个升级计划。没错。这个升级计划正在执行那些增量步骤。但现在我们有了映射,你可以看到 Demo 3 starter 也在升级计划中了。
所以,当我们执行 advisor upgrade plan apply 时,它会执行与这些升级相关的所有配方,其中可能包含一些跳跃,比如 Hibernate,也可能就是下一个增量版本。
是的,基本上,总结一下,我之前展示的那个很难手动制定的表格,这个工具能帮你自动生成。
我想基本上就是这样了。
总结与问答
Vicente Soriano: 是的,就是这样。我们的时间快到了。但如果你们想问我们任何问题,我们可以在这里。
我们两个都会在这里。非常感谢。
Raquel Pau: 谢谢。
