为什么以及如何升级至 Java 16 或 17
在 2021 年 4 月 27 日的 InfoQ 直播中,我探讨了为什么应该考虑升级到 Java 16 或 Java 17(一旦发布),并就如何完成升级提供了一些实用的建议。
直播的内容基于我个人的 GitHub 库 JavaUpgrades,其中有文档和示例介绍了升级到 Java 16 或 Java 17 时常见的难题和异常。其中也有具体的解决方案,你可以用在自己的应用程序中。示例要用 Docker 运行,是用 Maven 构建的,但是你当然也可以设置自己的 Gradle 构建。
本文以及那次直播都是为了让用户可以轻松升级到 Java 16 或 Java 17。大部分常见的升级任务都讨论到了,所以你可以更容易地解决它们,并专注于克服应用程序所特有的挑战。
Java 的每个新版本,尤其是大版本,都会解决安全漏洞,提升性能,增加新特性。保持 Java 版本最新有助于保持应用程序的健康,也有助于组织留住现有的开发人员,并有可能吸引来新员工,因为开发人员一般更希望使用比较新的技术。
人们认为,升级到 Java 的新版本需要很大的工作量。这是因为代码库需要变更,还需要在所有构建和运行应用程序的服务器中安装 Java 的最新版本。幸运的是,有些公司使用了 Docker,团队可以让它们自己升级这些内容。
许多人将 Java 9 模块系统(即 Jigsaw)视为一项重大的挑战。然而,Java 9 并不需要你显式地使用模块系统。事实上,大多数运行在 Java 9 以及更高版本上的应用程序并没有在代码库中配置 Java 模块。
评估任何升级所需的工作量都是一项挑战。那取决于多种因素,如依赖项数量及其现状。举例来说,如果你使用的是 Spring Boot,那么升级 Spring Boot 可能已经解决大部分升级问题。遗憾的是,由于存在不确定性,大部分开发人员会将升级工作量评估为许多天、周甚或是月。如此一来,考虑成本、时间或其他优先事项,组织或管理层就会推迟升级。我以前见过人们对将 Java 8 应用程序升级到 Java 11 的工作量评估从数周到数月不等。不过,我曾在几天内完成了一次类似的升级。这一部分是因为我之前的经验,不过,这也得益于我没有多想就开始了升级过程。周五下午升级 Java 就很理想,看看会发生什么。我最近将一个 Java 11 应用程序升级到了 Java 16,我唯一需要完成的任务就是升级一个 Lombok 依赖项。
升级可能很困难,评估所需的时间似乎是不可能的,但通常,实际的升级过程不会花那么多时间。在许多应用程序升级中,我都见过同样的问题。我希望帮助团队快速解决重复出现的问题,让他们可以集中精力克服应用程序独有的挑战。
过去,Java 每两年发布一个新版本。然而,从 Java 9 发布之后,新版本发布变成了每 6 个月一次,长期支持版本(LTS)每 3 年一次。大多数非长期支持版本都通过小版本升级提供大约 6 个月的支持,直到下一个版本发布。另一方面,LTS 版本几年内都会收到小版本升级,至少到下个 LTS 版本发布。实际提供支持的时间可能会更长,这取决于 OpenJDK 的供应商(Adoptium、Azul、Corretto 等)。举例来说,Azul 对于非 LTS 版本提供的支持时间就比较长。
你可能会问自己,“我应该总是升级到最新版本,还是应该停留在一个 LTS 版本上?”保证应用程序使用的是 LTS 版本意味着你可以利用小版本升级带来的各种改进,尤其是与安全相关的那些。另一方面,在使用最新的非 LTS 版本时,你应该每隔 6 个月就升级到一个新的非 LTS 版本,否则就无法利用小版本升级了。
然而,每 6 个月升一次级是一项不小的挑战,因为在升级应用程序之前,你可能不得不等待你所使用的框架完成升级。但是,你应该也不会等待太长时间,因为非 LTS 版本的小版本很快就会不再发布了。在我们公司,我们目前决定停留在 LTS 版本上,因为我们觉得自己没有时间每 6 个月升级一次,这样一个时间窗口太小。不过也不绝对,如果团队真得需要,或者一个非 LTS 版本带来了有趣的 Java 新特性,那么我们也可能改变决定。
一般来说,应用程序由依赖项和你自己的代码(打包后在 JDK 上运行)构成。如果 JDK 中有什么修改,那么依赖项或 / 和你自己的代码就需要修改。在大多数情况下,这是由 JDK 移除了某项特性导致的。如果你的依赖项使用了一项已经移除的 JDK 特性,那么请保持耐心,等待该依赖项的新版本发布。
当升级应用程序时,你可能希望使用 JDK 的不同版本,如最新版本用于实际的升级,老版本用于保持应用程序的运行。用于应用程序开发的当前 JDK 版本可以通过环境变量JAVA_HOME
指定,也可以借助包管理工具 SDKMAN! 或 JDKMon。
对于我 GitHub 库中的示例,我使用 Docker 和不同的 JDK 版本来说明特定的特性如何工作或造成破坏。你可以试一下相关特性,而不必安装多个 JDK 版本。遗憾的是,使用 Docker 容器的反馈回路有点长。需要首先构建并运行镜像。所以一般来说,我建议你尽可能从 IDE 内升级。但是,在一个干净的、没有个性化设置的 Docker 容器环境中试验一些东西或构建应用程序或许是一个不错的注意。
为了说明这一点,我们创建了一个标准的 Dockerfile 文件,其中包含下面的内容。该示例使用了 Maven JDK 17 镜像,并将你的应用程序代码复制到里面。RUN 命令会运行所有测试,出错了也不会失败。
FROM maven:3.8.1-openjdk-17-slim
ADD . /yourproject
WORKDIR /yourproject
RUN mvn test --fail-at-end
要想构建上述镜像,则运行docker build 命令,并通过-t 指定标签(或名称),通过.
配置上下文,在本例中是当前目录。
docker build -t javaupgrade .
大多数开发人员都是从升级本地环境开始,然后是构建服务器,最后是各部署环境。不过,我有时候会直接在构建服务器上使用新版本的 Java 进行构建,而不是针对这个特定的项目做好所有配置,然后看看会出什么问题。
一次性从 Java 8 升级到 17 也是可以的。不过,如果你遇到任何问题,可能会很难确定这两个 Java 版本间的哪个新特性导致了问题。小步升级,比如从 Java 8 升级到 Java 11,定位问题会比较容易。而且,在你搜索问题原因时,加上 Java 版本也是有帮助的。
我建议在旧版本的 Java 上升级依赖项。那样你可以专注于让依赖项可以正常工作,而不必同时升级 Java。遗憾的是,有时候没法这样做,因为有些依赖项需要更新的 Java 版本。如果是这样,你就别无选择,只能同时升级 Java 和依赖项了。
Maven 和 Gradle 提供了一些插件,可以显示依赖项的新版本。mvn versions:display-dependency-updates 命令会调用 Maven 版本插件。该插件会列出有新版本可用的依赖项:
[2.8.1:display-dependency-updates (default-cli) @ mockito_broken --- ] --- versions-maven-plugin:
[in Dependencies have newer versions: ] The following dependencies
[5.7.2 -> 5.8.0-M1 ] org.junit.jupiter:junit-jupiter ....................
[3.11.0 -> 3.11.2 ] org.mockito:mockito-junit-jupiter ...................
在build.gradle
文件中配置好插件后,gradle dependencyUpdates -Drevision=release
命令会调用 Gradle 版本插件:
plugins {
id "com.github.ben-manes.versions" version "$version"
}
升级完依赖项后,就可以升级 Java 了。要想把代码改到在新版本的 Java 上运行,最好是在 IDE 中进行,以确保它支持 Java 的最新版本。最后,将构建工具升级到最新版本,并配置 Java 版本:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
plugins {
java {
toolchain {
languageVersion = JavaLanguageVersion.of(16)
}
}
}
compile 'org.apache.maven.plugins:maven-compiler-plugin:3.8.1'
不要忘了把 Maven 和 Gradle 插件升级到最新版本。
JDK 中总是有些元素可能被移除,包括方法、证书、垃圾收集算法、JVM 选项,甚至是整个工具。不过,在大多数情况下,这些被移除的部分在删除之前已经被标记为“已废弃”或“将移除”。举例来说,JAXB 在 Java 9 中已废弃,但最终移除是在 Java 11 中。如果你已经解决了与已废弃的特性相关的问题,那么在特性真正被移除时也就不用担心了。
可以参考 Java Version Almanac 和 Foojay Almanac 对 Java 不同版本的比较,看看增加了哪些项,废弃了哪些项,或者是移除了哪些项。以 Java 增强提案(JEP) 这种形式所做的高级变更可以在 OpenJDK 网站上查看。关于每个 Java 版本的详细信息,可以查阅 Oracle 公布的发布说明。
Java 11 移除了多个特性。首先是 JavaFX,它已经不在规范中,也不再捆绑在 OpenJDK 中。不过,有的供应商提供的 JDK 构建包含的内容比规范里的多。例如,ojdkbuild 和 Liberica JDK 的完整 JDK 都包含了 OpenJFX。此外,你也可以使用 Gluon 提供的 JavaFX 构建,或者向应用程序添加 OpenJFX 依赖。
在 JDK 11 之前,有些字体是包含在 JDK 中的。例如,Apache POI 可以把这些字体用于 Word 和 Excel 文档。然而,在 JDK 11 开始,就不再提供那些字体了。如果操作系统也没有提供,那么你可能就会遇到一些奇怪的错误。解决方案是在操作系统上安装字体。根据你在应用程序中使用的字体,你可能需要安装更多的包:
apt install fontconfig
Optional: libfreetype6 fontconfig fonts-dejavu
Java Mission Control(JMC)是一个监控和性能分析应用程序,它开销很小,可以在包括生产环境在内的任何环境中对应用程序做性能分析。如果你没用过,我强烈建议你用一下。它不再是 JDK 的一部分,但 AdoptOpenJDK 和 Oracle 给它起了一个新名字 JDK Mission Control,并提供了单独的下载包。Java 11 的最大变化是移除了 Java EE 和 CORBA 模块,如 4 个 Web 服务 API——JAX-WS、JAXB、JAF 和 Common Annotations——因为已经包含在 Java EE 中,所以被认为是多余的。在 2017 年发布后不久,Oracle 就将 Java EE 8 贡献给了 Eclipse 基金会,旨在使 Java EE 开源。考虑到 Oracle 的品牌策略,有必要将 Java EE 重命名为 Jakarta EE,并将命名空间从 javax
迁移到 jakarta
。因此,在使用像 JAXB 这样的依赖项时,确保自己使用了比较新的 Jakarta EE 工件。例如,JAXB 工件的 Java EE 8 版本名为javax.xml.bind:jaxb-api
,后续开发于 2018 年停止。JAXB 的 Jakarta EE 版本在新工件jakarta.xml.bind:jakarta.xml.bind-api
下继续开发。务必确保应用程序中所有的导入都已经改为了新命名空间jakarta
。例如,对于 JAXB,将javax.xml.bind.*
改为jakarta.xml.bind.*
,并添加相关依赖项。下图中左边的列是受这项变更影响的模块。右边两列显示了可以用作依赖项的groupId
和artifactId
。请注意,JAXB 和 JAX-WS 都需要两个依赖项:一个用于 API,一个用于实现。官方没有提供 CORBA 的替代方案,但 Glassfish 还是提供了一个可用的工件。
Java 15 移除了 JavaScript 引擎 Nashorn,不过,你仍然可以通过添加以下依赖项来使用:
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.2</version>
</dependency>
在这个版本中,JDK 开发者封装了一些 JDK 内部构件。他们不希望应用程序再使用 JDK 的底层 API。这主要影响了 Lombok 这样的工具。所幸,Lombok 几个周内就发布了一个新版本,解决了这个问题。
如果你有任何代码或依赖项仍然使用 JDK 内部构件,那么可以尝试使用 JDK 的高级 API 来解决这个问题。如果不行的话,Maven 还提供了一种变通方法:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<fork>true</fork>
<compilerArgs>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
我曾尝试使用 Maven Toolchains 通过在pom.xml
文件中指定 JDK 版本来实现 JDK 切换。很遗憾,当使用 Lombok 的旧版本在 Java 16 上运行应用程序时报错了:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project broken: Compilation failure -> [Help 1]
上面就是全部报错信息。我不知道你怎么看,但在我看来,这没什么用,所以我提交了这个问题。如果这个问题修复了,那么使用 Maven Toolchains 切换版本是一种不错的方法。后来,我直接在 Java 16 上运行代码,得到了一个更具描述性的错误,其中提到了我之前展示的部分变通方案:
… class lombok.javac.apt.LombokProcessor (in unnamed module @0x21bd20ee) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment
(in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing
to unnamed module …
JDK 维护人员已经就 9 月份要发布的内容 达成了一致。Applet API 将被废弃,因为浏览器停止支持 Applet 已经很长时间了。实验性的 AOT 和 JIT 编译器也将被移除。作为实验性编译器的替代方案,你可以使用 GraalVM。最大的变化是 JEP-403:强封装的 JDK 内部构件。Java 选项--illegal-access
已经无效,如果你仍然试图访问一个内部 API,则会抛出如下异常:
java.lang.reflect.InaccessibleObjectException:
Unable to make field private final {type} accessible:
module java.base does not "opens {module}" to unnamed module {module}
大多数时候,这可以通过升级依赖项或使用高级 API 来解决。如果不行的话,你可以使用--add-opens
参数来获得对内部 API 的访问。不过,除非不得已不要这样做。注意,有些工具在 Java 17 上还无法运行。例如,Gradle 就无法构建项目,而 Kotlin 不能使用jvmTarget = "17"
。有些框架,如 Mockito,在 Java 17 上也有些小问题。enum
字段中的方法会导致这个特定的问题。不过,我估计大部分问题都会在 Java 17 发布之前或发布之后短期内得到解决。对于任何插件或依赖项,你可能会在构建应用程序时看到这条消息“不支持的类文件主版本 61”。类文件主版本 61 用于 Java 17,60 用于 Java 16。这基本上是说该插件或依赖项不能用于那个 Java 版本。大多数时候,升级到最新版本就可以解决问题。
在解决了所有挑战之后,你终于可以在 Java 17 上运行应用程序了。经过努力,你现在可以使用令人兴奋的 Java 新特性了,如记录和模式匹配。
升级 Java 是一项挑战,不过这也要看你的 Java 版本和依赖项有多老,你的环境配置有多复杂。本文旨在帮助你解决 Java 升级时最常见的挑战。一般来说,很难评估实际的升级工作要花费多长时间。我觉得,大多数时候,从 Java 11 升级到 Java 17 要比从 Java 8 升级到 Java 11 简单。对于大多数应用程序,从一个 LTS 版本升级到下一个 LTS 版本需要几个小时到几天的时间。大部分时间都花在了构建应用程序上。重要的是先开始,然后逐步更改。这样可以激励自己、团队和管理层继续努力。
你开始升级应用程序了吗?
作者简介:
Johan Janssen 是 Sanoma Learning 教育部门的一名软件架构师。他特别喜欢分享 Java 相关的知识。他在 Devoxx、Oracle Code One、Devnexus 等会议上做过演讲。他通过参与计划委员会来协助大会组织,发起并组织了 JVMCON。他得过的奖项有 JavaOne Rock Star 和 Oracle Code One Star。他在数字和印刷媒体上撰写了各种文章。他是 Chocolatey 各种 Java JDK/JRE 包的维护者,每月有大约 10 万次下载。
原文链接:
https://www.infoq.com/articles/why-how-upgrade-java17/