作者 | Donna 、Jan Luehe

译者 | 刘雅梦

是首批大规模采用 11 的大型企业之一,在 2018 年底 11 发布后不久, 就开始了 11 的采用之旅。

前沿吗?当然是。

安全吗?绝对地。

你可能还不知道, 在整合前沿、转型技术,并以安全、可靠、无缝的方式,同时在不损害其核心价值:信任 的前提下,将这些技术提供给客户方面一直处于行业的领先地位。从 gRPC 到 , 在新技术领域有着早期大胆探索的历史。

在本文的案例中,将主要的 CRM 应用程序升级到 11 是一项庞大的跨组织工作。把它做好,不仅能为我们带来几年的 Java 运行时创新红利,也能为我们的客户提供更好的体验,并为开源社区做出贡献。

1背景

2018 年末, 11 作为 Java 最新的长期支持(LTS)版本面世。这开启了一个人们期待已久的机遇,推动了 应用程序的向前发展,并为我们的内部开发人员带来了巨大的新特性和创新。

为什么我们认为我们可以安全地从 8(上一个最新的 LTS 版本)过渡到 11 呢?

首先,我们并不是一蹴而就的。对于 9 和 10 这两个版本,虽然我们只打算将它们作为垫脚石,而不在生产环境中使用,但一旦它们的版本可用,我们就会立即升级到对应版本。正如你所料,最困难的部分是从 8 升级到 9,这需要对 应用程序进行重大的更改。从 9 升级到 10,再从 10 升级到 11,都只需做相对较小的改动。

另外,Java 的向后兼容性保证允许用旧版本 Java 开发编译的应用程序代码能运行在新的版本上,这一功能的威力不容小觑。向后兼容性为迁移提供了巨大的帮助,这样我们的大多数代码都不需要更改。

应用程序利用 Java 向后兼容性的方法之一是,将用于构建 应用程序的 Java 版本与用于启动它的 Java 版本分开。这使我们能够首先集中精力将过程的一端从 8 升级到 11,而另一端仍保持在 8 上不变,并将其升级到 11 的时间往后推迟。我们内部开发人员的目标是,通过在初始化和启动 应用程序的脚本中隐藏所有的差异和复杂性的方式,让 8 和 11 运行时之间的切换尽可能的简单和无缝。因此,对于我们的开发人员来说,升级到 11 运行时就像用 11 版本的字符串覆盖配置属性一样简单。

也就是说,我们面临的另一个挑战是,我们的 11 迁移工作跨越了多个版本周期,我们必须确保任何支持 11 的增量更改都不会打破我们的生产环境(生产环境仍然是基于 8 部署的),也不能对客户的信任产生任何负面影响。

2平台的变更 & 挑战

当我们一个接一个地升级 版本时,我们遇到了 Java 平台的许多显著变更。而我们的迁移之路漫长且需有条不紊地推进,这意味着这些变更会给我们带来了很多挑战,但在这里我们只讨论其中的几个。

类路径与模块化

Java SE 9 平台引入的重大变更之一就是 Java 平台模块系统(Java ,JPMS)。JPMS 将 JDK 划分为多个模块,每个模块都是一组命名唯一且可重用的相关包。

好消息是,Java 9 仍然支持传统的类路径,它能与模块路径一起工作,并映射到一个被称为未命名模块的特殊模块上。因此,构成 应用程序类路径的所有 JAR 文件都会自动加入模块系统,从而导致了传统类路径和模块路径的混合。

实际上, 应用程序的整个类加载器层次结构都保留在 Java 9 及更高版本中。它由我们的 Web 服务器和 容器锚定,委托给 OSGi 类加载器,而 OSGi 类加载器又委托给 Java 运行时的内置类加载器。

然而,作为 计划的一部分,Java 9 带来了一个影响类加载的重大变更。这是对授权标准覆盖机制( ,用于支持加载包含授权标准和独立技术实现的 JAR 文件)和扩展机制( ,用于支持加载包含扩展或可选软件包的 JAR 文件)的移除。由于 应用程序过去依赖于这两种机制,因此必须使用 --path 、 ---path 和 -patch- 标志的组合,将所有受影响的 JAR 文件迁移到 应用程序的模块路径下。不过,这些非模块化的 JAR 文件都无需转换为模块:它们作为依赖项被放置在 应用程序的模块路径上,从而自动成为模块化的。此功能被称为自动模块化,创建它是为了减轻将现有应用程序转换为新模块系统的负担。

影响 应用程序的另一个变更是删除了 应用程序所依赖的 Java (“Java EE”)API。Java 9 开始将这些 API 分离到它们各自的模块中,这些模块被注解为不推荐使用,以便删除,这表明了在将来的版本中会删除它们的意图。这些模块包含在运行时镜像中,但默认情况下未启用。因而java委托,它们必须通过 --add 标识显式“激活”。

从 开始,这些模块不再包含在运行时中(参见 JEP 320:删除 Java EE 和 CORBA 模块)。相反,Java EE 和 CORBA 技术的独立版本作为 Maven 构件发布,并可以从第三方网站(如 Maven )上获取,我们从那里下载了它们并将它们添加到了 应用程序的模块路径中。

向后不兼容

在将 应用程序的 Java 运行时迁移到 11 时,我们发现了许多向后不兼容的变更。其中大多数都是“设计使然”,并且是涵盖在版本说明中了的,正如下面所要讨论的那样。(有一个 true 的回归影响了布尔型 bean 属性的内省;这是由 实现本身的一个 bug 引起的,我们报告了这个 bug,并且它已经被修复了。)

设计上向后不兼容变更的例子很明显,因为它会导致 JVM 在启动时中断,并出现如下的错误:

Unrecognized VM option ''Error: Could not create the Java Virtual Machine.Error: A fatal exception has occurred. Program will exit.

引发该错误的原因是 应用程序一直在使用一些 Java 9 以后不再支持的垃圾回收(GC)选项。某些受影响的 GC 选项(例如 )在 JDK 8(JEP 173)中已被弃用了,而在 Java 9(JEP 214)中已经被移除了。其他的java委托,包括 和 在内 ,由于已经在 Java 9 中重新实现了 GC 日志(请参阅 JEP 271)以便使用 JEP 158 中引入的 JVM 框架,它们都变成非法的了。

接下来的挑战就变成,继续为仍运行在 8 上的 生产实例提供这些 GC 选项的支持,同时避免这些选项用在已经升级到 11 的 生产实例上。

我们采用了一种可扩展方法,在启动 应用程序之前,扩充负责组装该应用程序的 JVM 参数列表的 ant 目标,这样,当 Java 运行时被设置为 11 时,它会过滤掉(使用 ant 语法)所有不受支持的 GC 选项。事实证明,这种方法非常灵活,允许我们将选定的 生产实例升级到 11,并在需要时回滚到 8。一旦 11 成为新的默认 Java 运行时,并且所有的生产实例都已经成功迁移,过滤器就可以从 ant 目标中移除了。

虽然影响 GC 选项的变更明显破坏了 应用程序,但其他设计上的变更却以更微妙的方式对应用程序造成了破坏。其中一个变更影响了 fork/join 公共池线程的上下文类加载器,它不再继承任务提交线程的上下文类加载器,而是使用系统类加载器进行初始化。JDK 9 版本说明中涵盖了这一变更,并提供了恢复以前行为的解决方法。这一变更的影响在 应用程序中以许多不同的方式表现出来了。

其他设计方面的变更影响来自核心库的 Java 语言 API,核心库的实现已经被更改,以便更严格地执行其原始 API 契约。其中一个例子是 ,它的实现已经被增强以防止可重入使用,其中传递给 () 的映射函数修改了调用 () 的映射。以前,这种情况并未引起注意,但可能会使映射(map)处于不一致的状态。但是,从 9 开始,它会被检测到并被标记成 。

3第三方依赖 & 开源贡献

除了升级 外,我们还需要升级 应用程序的一些底层第三方依赖。如果你忽略团队为升级 而修改 2700 多个 Java 测试类的时间,那么大部分工作都是相当简单的。也就是说,作为 11 的早期使用者,考虑到 应用程序的复杂性,我们有望在开发过程中解决一些 bug。这就为向开源社区贡献一些修复程序带来了很好的机会。

OSGi

OSGi 就为我们带来了一个机会,在启动过程中,我们遇到了 javax. 的问题。javax. 包是受 JEP 320 影响的包之一,JEP 320 对 Java SE 11 生效,并从 JDK 中删除了这个包和所有其他 Java EE 和 CORBA 包。按照该 JEP 的建议,我们已经将所有提供缺失包的 JAR 文件(包括 javax.-api.jar )添加到了 应用程序的模块路径中,在那里它们将被视为自动模块(请参见上文)。根据 JPMS 规范,自动模块应该导出其所有的包——显然在我们的例子中不会发生这种情况!

事实证明,我们在 OSGi 框架的包解析逻辑中发现了一个 bug(违反了 JPMS 规范)。我们向管理 OSGi 项目的 基金会报告了这个问题,并提交了一个修复程序。我们的修复程序可以确保将自动模块的所有包自动添加到 VM 提供的包列表中,它被接受并被合并发布到了 OSGi 社区。

作为检入 应用程序代码变更的一部分,开发人员将其变更列表(CL)提交给预签入(Pre-),预检入会对其进行检查以确保 CL 不会将任何重复的类引入到 应用程序的类路径中。重复的类是指具有相同 FQCN 但内容不同的类。预检入的重复类查找器(-Class-,DCF)依赖于 的 Java 反编译器,该反编译器使用给定的 FQCN 搜索和反编译类,能在类路径上搜索 JAR 文件列表。

DCF 已经被集成到 应用程序中,并从该应用程序继承了它的 Java 运行时。当在 11 运行时上执行时, 的反编译器会失败。我们向 报告了这个问题,并提交了一个简化可执行的测试用例来重现该问题。 开发人员重现并修复了这个问题,解除了 应用程序当 Java 运行时设置为 11 时的预检入阻塞问题。

4首发优势

Multi- JAR 文件

正如前文所述, 应用程序利用 Java 向后兼容性的方法之一是,将用于构建 应用程序的 Java 版本与用于启动它的 Java 版本分开。这样可以隔离风险,因此,即使 应用程序及其依赖项仍然是使用 8 构建的,在运行时,我们也可以利用一些从 Java 9 才开始添加的新的核心 Java API(例如,JEP 259 引入的新 stack- API),利用 Multi- 文件(JEP 238)的优势。

Multi- JAR 是在 Java 9 中引入的一个新特性:它扩展了 JAR 文件的格式,允许同一 Java 类资源的多个版本共存于同一 JAR 文件中,其中该类的每个版本可以是以不同方式实现并根据不同 JDK 版本编译的。

支持多版本的类加载器会从多版本 JAR 文件中自动加载适当的类(即,那些与 Java 运行时 JDK 版本相匹配的类)。我们的 容器和 OSGi 类加载器都支持多版本 JAR 文件,并且随着 JDK 11 及以上版本的广泛使用,我们预计将有越来越多的第三方依赖项会使用这种格式打包。

内置的性能改进

“紧凑字符串”(JEP 254)为我们提供了一个免费的性能优化。这个特性最初是在 Java 9 中引入的,它通过将字符(char)数组迁移到更紧凑的字节(byte)数组(加上一个编码标识字段)来提供更节省内存的字符串内部表示。这降低了字符串的总体堆使用和内存压力,进而对垃圾回收和应用程序整体性能产生了积极影响。

监控改进

Java (JFR)是一种分析工具,用于从正在运行的 Java 应用程序中收集诊断信息并分析数据。JFR 过去只作为商业 JDK 插件提供,但是从 11 开始,它就与 Java 一起开源了。现在可以在单个 应用程序服务器实例上启用 JFR 来解决性能问题,这是一个巨大的利好。

5期待

11 的升级发布没有出现任何大的问题。它的推出从开始到结束大约花了 6 个月的时间,遵循了我们通常所遵循的经过充分审查的分散策略,以减轻客户影响。

在推出完成后不久,我们就将重点转移到了用于构建 应用程序的 Java 版本上。它仍然被设置为 8,然后我们也将它升级到了 11。通过将应用程序的编译时版本升级到 11,我们的开发人员可以使用自 Java 9 以来引入的所有新的 Java 语言功能,其中包括新的 stack- API、新的 HTTP 客户端、对 try-with- 语句的改进、允许在接口中使用私有方法、 和 类中的新方法、 的改进、用于局部变量类型推断的新 var 关键字,等等。

我们期望这些新的 Java 语言特性能给我们带来显著的生产力提升和创新收益。将运行时和编译时的 Java 版本升级到 11 使我们能够更快、更无缝地采用未来的 Java 版本。

附加资源

JDK 版本说明

Java 平台,标准版 JDK 9 迁移指南

#JSMIG-GUID--5899-4FB2-B34E-

JSR 376(Java 平台模块系统)

原文链接:

今日好文推荐

每周精要上线移动端,立刻订阅,你将获得

InfoQ 用户每周必看的精华内容集合:

资深技术编辑撰写或编译的全球 IT 要闻;

一线技术专家撰写的实操技术案例;

InfoQ 出品的课程和技术活动报名通道;

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注