观点 | Opinion
为什么以及如何升级至Java 16或17
作者 Johan Janssen 译者 平川
在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每两年发布一个新版本。然而,从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的不同版本,如最新版本用于实际的升级,老版本用于保持应用程序的运行。用于应用程序开发的当前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版本插件。该插件会列出有新版本可用的依赖项:
[INFO] --- versions-maven-plugin:2.8.1:display-dependency-updates (default-cli) @ mockito_broken ---
[INFO] The following dependencies in Dependencies have newer versions:
[INFO] org.junit.jupiter:junit-jupiter .................... 5.7.2 -> 5.8.0-M1
[INFO] org.mockito:mockito-junit-jupiter ................... 3.11.0 -> 3.11.2
在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中移除的特性
JDK中总是有些元素可能被移除,包括方法、证书、垃圾收集算法、JVM选项,甚至是整个工具。不过,在大多数情况下,这些被移除的部分在删除之前已经被标记为“已废弃”或“将移除”。举例来说,JAXB在Java 9中已废弃,但最终移除是在Java 11中。如果你已经解决了与已废弃的特性相关的问题,那么在特性真正被移除时也就不用担心了。
可以参考Java Version Almanac和Foojay Almanac对Java不同版本的比较,看看增加了哪些项,废弃了哪些项,或者是移除了哪些项。以Java增强提案(JEP)这种形式所做的高级变更可以在OpenJDK网站上查看。关于每个Java版本的详细信息,可以查阅Oracle公布的发布说明。
Java 11
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
Java 15移除了JavaScript引擎Nashorn,不过,你仍然可以通过添加以下依赖项来使用:
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.2</version>
</dependency>
Java 16
在这个版本中,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 …
Java 17
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万次下载。
原文链接