為什么以及如何升級(jí)至 Java 16 或 17
在 2021 年 4 月 27 日的 InfoQ 直播中,我探討了為什么應(yīng)該考慮升級(jí)到 Java 16 或 Java 17(一旦發(fā)布),并就如何完成升級(jí)提供了一些實(shí)用的建議。
直播的內(nèi)容基于我個(gè)人的 GitHub 庫(kù) JavaUpgrades,其中有文檔和示例介紹了升級(jí)到 Java 16 或 Java 17 時(shí)常見的難題和異常。其中也有具體的解決方案,你可以用在自己的應(yīng)用程序中。示例要用 Docker 運(yùn)行,是用 Maven 構(gòu)建的,但是你當(dāng)然也可以設(shè)置自己的 Gradle 構(gòu)建。
本文以及那次直播都是為了讓用戶可以輕松升級(jí)到 Java 16 或 Java 17。大部分常見的升級(jí)任務(wù)都討論到了,所以你可以更容易地解決它們,并專注于克服應(yīng)用程序所特有的挑戰(zhàn)。
Java 的每個(gè)新版本,尤其是大版本,都會(huì)解決安全漏洞,提升性能,增加新特性。保持 Java 版本最新有助于保持應(yīng)用程序的健康,也有助于組織留住現(xiàn)有的開發(fā)人員,并有可能吸引來(lái)新員工,因?yàn)殚_發(fā)人員一般更希望使用比較新的技術(shù)。
人們認(rèn)為,升級(jí)到 Java 的新版本需要很大的工作量。這是因?yàn)榇a庫(kù)需要變更,還需要在所有構(gòu)建和運(yùn)行應(yīng)用程序的服務(wù)器中安裝 Java 的最新版本。幸運(yùn)的是,有些公司使用了 Docker,團(tuán)隊(duì)可以讓它們自己升級(jí)這些內(nèi)容。
許多人將 Java 9 模塊系統(tǒng)(即 Jigsaw)視為一項(xiàng)重大的挑戰(zhàn)。然而,Java 9 并不需要你顯式地使用模塊系統(tǒng)。事實(shí)上,大多數(shù)運(yùn)行在 Java 9 以及更高版本上的應(yīng)用程序并沒(méi)有在代碼庫(kù)中配置 Java 模塊。
評(píng)估任何升級(jí)所需的工作量都是一項(xiàng)挑戰(zhàn)。那取決于多種因素,如依賴項(xiàng)數(shù)量及其現(xiàn)狀。舉例來(lái)說(shuō),如果你使用的是 Spring Boot,那么升級(jí) Spring Boot 可能已經(jīng)解決大部分升級(jí)問(wèn)題。遺憾的是,由于存在不確定性,大部分開發(fā)人員會(huì)將升級(jí)工作量評(píng)估為許多天、周甚或是月。如此一來(lái),考慮成本、時(shí)間或其他優(yōu)先事項(xiàng),組織或管理層就會(huì)推遲升級(jí)。我以前見過(guò)人們對(duì)將 Java 8 應(yīng)用程序升級(jí)到 Java 11 的工作量評(píng)估從數(shù)周到數(shù)月不等。不過(guò),我曾在幾天內(nèi)完成了一次類似的升級(jí)。這一部分是因?yàn)槲抑暗慕?jīng)驗(yàn),不過(guò),這也得益于我沒(méi)有多想就開始了升級(jí)過(guò)程。周五下午升級(jí) Java 就很理想,看看會(huì)發(fā)生什么。我最近將一個(gè) Java 11 應(yīng)用程序升級(jí)到了 Java 16,我唯一需要完成的任務(wù)就是升級(jí)一個(gè) Lombok 依賴項(xiàng)。
升級(jí)可能很困難,評(píng)估所需的時(shí)間似乎是不可能的,但通常,實(shí)際的升級(jí)過(guò)程不會(huì)花那么多時(shí)間。在許多應(yīng)用程序升級(jí)中,我都見過(guò)同樣的問(wèn)題。我希望幫助團(tuán)隊(duì)快速解決重復(fù)出現(xiàn)的問(wèn)題,讓他們可以集中精力克服應(yīng)用程序獨(dú)有的挑戰(zhàn)。
過(guò)去,Java 每?jī)赡臧l(fā)布一個(gè)新版本。然而,從 Java 9 發(fā)布之后,新版本發(fā)布變成了每 6 個(gè)月一次,長(zhǎng)期支持版本(LTS)每 3 年一次。大多數(shù)非長(zhǎng)期支持版本都通過(guò)小版本升級(jí)提供大約 6 個(gè)月的支持,直到下一個(gè)版本發(fā)布。另一方面,LTS 版本幾年內(nèi)都會(huì)收到小版本升級(jí),至少到下個(gè) LTS 版本發(fā)布。實(shí)際提供支持的時(shí)間可能會(huì)更長(zhǎng),這取決于 OpenJDK 的供應(yīng)商(Adoptium、Azul、Corretto 等)。舉例來(lái)說(shuō),Azul 對(duì)于非 LTS 版本提供的支持時(shí)間就比較長(zhǎng)。

你可能會(huì)問(wèn)自己,“我應(yīng)該總是升級(jí)到最新版本,還是應(yīng)該停留在一個(gè) LTS 版本上?”保證應(yīng)用程序使用的是 LTS 版本意味著你可以利用小版本升級(jí)帶來(lái)的各種改進(jìn),尤其是與安全相關(guān)的那些。另一方面,在使用最新的非 LTS 版本時(shí),你應(yīng)該每隔 6 個(gè)月就升級(jí)到一個(gè)新的非 LTS 版本,否則就無(wú)法利用小版本升級(jí)了。
然而,每 6 個(gè)月升一次級(jí)是一項(xiàng)不小的挑戰(zhàn),因?yàn)樵谏?jí)應(yīng)用程序之前,你可能不得不等待你所使用的框架完成升級(jí)。但是,你應(yīng)該也不會(huì)等待太長(zhǎng)時(shí)間,因?yàn)榉?LTS 版本的小版本很快就會(huì)不再發(fā)布了。在我們公司,我們目前決定停留在 LTS 版本上,因?yàn)槲覀冇X(jué)得自己沒(méi)有時(shí)間每 6 個(gè)月升級(jí)一次,這樣一個(gè)時(shí)間窗口太小。不過(guò)也不絕對(duì),如果團(tuán)隊(duì)真得需要,或者一個(gè)非 LTS 版本帶來(lái)了有趣的 Java 新特性,那么我們也可能改變決定。
一般來(lái)說(shuō),應(yīng)用程序由依賴項(xiàng)和你自己的代碼(打包后在 JDK 上運(yùn)行)構(gòu)成。如果 JDK 中有什么修改,那么依賴項(xiàng)或 / 和你自己的代碼就需要修改。在大多數(shù)情況下,這是由 JDK 移除了某項(xiàng)特性導(dǎo)致的。如果你的依賴項(xiàng)使用了一項(xiàng)已經(jīng)移除的 JDK 特性,那么請(qǐng)保持耐心,等待該依賴項(xiàng)的新版本發(fā)布。
當(dāng)升級(jí)應(yīng)用程序時(shí),你可能希望使用 JDK 的不同版本,如最新版本用于實(shí)際的升級(jí),老版本用于保持應(yīng)用程序的運(yùn)行。用于應(yīng)用程序開發(fā)的當(dāng)前 JDK 版本可以通過(guò)環(huán)境變量JAVA_HOME指定,也可以借助包管理工具 SDKMAN! 或 JDKMon。
對(duì)于我 GitHub 庫(kù)中的示例,我使用 Docker 和不同的 JDK 版本來(lái)說(shuō)明特定的特性如何工作或造成破壞。你可以試一下相關(guān)特性,而不必安裝多個(gè) JDK 版本。遺憾的是,使用 Docker 容器的反饋回路有點(diǎn)長(zhǎng)。需要首先構(gòu)建并運(yùn)行鏡像。所以一般來(lái)說(shuō),我建議你盡可能從 IDE 內(nèi)升級(jí)。但是,在一個(gè)干凈的、沒(méi)有個(gè)性化設(shè)置的 Docker 容器環(huán)境中試驗(yàn)一些東西或構(gòu)建應(yīng)用程序或許是一個(gè)不錯(cuò)的注意。
為了說(shuō)明這一點(diǎn),我們創(chuàng)建了一個(gè)標(biāo)準(zhǔn)的 Dockerfile 文件,其中包含下面的內(nèi)容。該示例使用了 Maven JDK 17 鏡像,并將你的應(yīng)用程序代碼復(fù)制到里面。RUN 命令會(huì)運(yùn)行所有測(cè)試,出錯(cuò)了也不會(huì)失敗。
FROM maven:3.8.1-openjdk-17-slimADD . /yourprojectWORKDIR /yourprojectRUN mvn test --fail-at-end
要想構(gòu)建上述鏡像,則運(yùn)行docker build 命令,并通過(guò)-t 指定標(biāo)簽(或名稱),通過(guò). 配置上下文,在本例中是當(dāng)前目錄。
docker build -t javaupgrade .大多數(shù)開發(fā)人員都是從升級(jí)本地環(huán)境開始,然后是構(gòu)建服務(wù)器,最后是各部署環(huán)境。不過(guò),我有時(shí)候會(huì)直接在構(gòu)建服務(wù)器上使用新版本的 Java 進(jìn)行構(gòu)建,而不是針對(duì)這個(gè)特定的項(xiàng)目做好所有配置,然后看看會(huì)出什么問(wèn)題。
一次性從 Java 8 升級(jí)到 17 也是可以的。不過(guò),如果你遇到任何問(wèn)題,可能會(huì)很難確定這兩個(gè) Java 版本間的哪個(gè)新特性導(dǎo)致了問(wèn)題。小步升級(jí),比如從 Java 8 升級(jí)到 Java 11,定位問(wèn)題會(huì)比較容易。而且,在你搜索問(wèn)題原因時(shí),加上 Java 版本也是有幫助的。
我建議在舊版本的 Java 上升級(jí)依賴項(xiàng)。那樣你可以專注于讓依賴項(xiàng)可以正常工作,而不必同時(shí)升級(jí) Java。遺憾的是,有時(shí)候沒(méi)法這樣做,因?yàn)橛行┮蕾図?xiàng)需要更新的 Java 版本。如果是這樣,你就別無(wú)選擇,只能同時(shí)升級(jí) Java 和依賴項(xiàng)了。
Maven 和 Gradle 提供了一些插件,可以顯示依賴項(xiàng)的新版本。mvn versions:display-dependency-updates 命令會(huì)調(diào)用 Maven 版本插件。該插件會(huì)列出有新版本可用的依賴項(xiàng):
[] --- versions-maven-plugin:2.8.1:display-dependency-updates (default-cli) @ mockito_broken ---[] The following dependencies in Dependencies have newer versions:[] org.junit.jupiter:junit-jupiter .................... 5.7.2 -> 5.8.0-M1[] org.mockito:mockito-junit-jupiter ................... 3.11.0 -> 3.11.2
在build.gradle 文件中配置好插件后,gradle dependencyUpdates -Drevision=release 命令會(huì)調(diào)用 Gradle 版本插件:
plugins {id "com.github.ben-manes.versions" version "$version"}
升級(jí)完依賴項(xiàng)后,就可以升級(jí) Java 了。要想把代碼改到在新版本的 Java 上運(yùn)行,最好是在 IDE 中進(jìn)行,以確保它支持 Java 的最新版本。最后,將構(gòu)建工具升級(jí)到最新版本,并配置 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 插件升級(jí)到最新版本。
JDK 中總是有些元素可能被移除,包括方法、證書、垃圾收集算法、JVM 選項(xiàng),甚至是整個(gè)工具。不過(guò),在大多數(shù)情況下,這些被移除的部分在刪除之前已經(jīng)被標(biāo)記為“已廢棄”或“將移除”。舉例來(lái)說(shuō),JAXB 在 Java 9 中已廢棄,但最終移除是在 Java 11 中。如果你已經(jīng)解決了與已廢棄的特性相關(guān)的問(wèn)題,那么在特性真正被移除時(shí)也就不用擔(dān)心了。
可以參考 Java Version Almanac 和 Foojay Almanac 對(duì) Java 不同版本的比較,看看增加了哪些項(xiàng),廢棄了哪些項(xiàng),或者是移除了哪些項(xiàng)。以 Java 增強(qiáng)提案(JEP) 這種形式所做的高級(jí)變更可以在 OpenJDK 網(wǎng)站上查看。關(guān)于每個(gè) Java 版本的詳細(xì)信息,可以查閱 Oracle 公布的發(fā)布說(shuō)明。
Java 11 移除了多個(gè)特性。首先是 JavaFX,它已經(jīng)不在規(guī)范中,也不再捆綁在 OpenJDK 中。不過(guò),有的供應(yīng)商提供的 JDK 構(gòu)建包含的內(nèi)容比規(guī)范里的多。例如,ojdkbuild 和 Liberica JDK 的完整 JDK 都包含了 OpenJFX。此外,你也可以使用 Gluon 提供的 JavaFX 構(gòu)建,或者向應(yīng)用程序添加 OpenJFX 依賴。
在 JDK 11 之前,有些字體是包含在 JDK 中的。例如,Apache POI 可以把這些字體用于 Word 和 Excel 文檔。然而,在 JDK 11 開始,就不再提供那些字體了。如果操作系統(tǒng)也沒(méi)有提供,那么你可能就會(huì)遇到一些奇怪的錯(cuò)誤。解決方案是在操作系統(tǒng)上安裝字體。根據(jù)你在應(yīng)用程序中使用的字體,你可能需要安裝更多的包:
apt install fontconfigOptional: libfreetype6 fontconfig fonts-dejavu
Java Mission Control(JMC)是一個(gè)監(jiān)控和性能分析應(yīng)用程序,它開銷很小,可以在包括生產(chǎn)環(huán)境在內(nèi)的任何環(huán)境中對(duì)應(yīng)用程序做性能分析。如果你沒(méi)用過(guò),我強(qiáng)烈建議你用一下。它不再是 JDK 的一部分,但 AdoptOpenJDK 和 Oracle 給它起了一個(gè)新名字 JDK Mission Control,并提供了單獨(dú)的下載包。Java 11 的最大變化是移除了 Java EE 和 CORBA 模塊,如 4 個(gè) Web 服務(wù) API——JAX-WS、JAXB、JAF 和 Common Annotations——因?yàn)橐呀?jīng)包含在 Java EE 中,所以被認(rèn)為是多余的。在 2017 年發(fā)布后不久,Oracle 就將 Java EE 8 貢獻(xiàn)給了 Eclipse 基金會(huì),旨在使 Java EE 開源。考慮到 Oracle 的品牌策略,有必要將 Java EE 重命名為 Jakarta EE,并將命名空間從 javax 遷移到 jakarta。因此,在使用像 JAXB 這樣的依賴項(xiàng)時(shí),確保自己使用了比較新的 Jakarta EE 工件。例如,JAXB 工件的 Java EE 8 版本名為javax.xml.bind:jaxb-api ,后續(xù)開發(fā)于 2018 年停止。JAXB 的 Jakarta EE 版本在新工件jakarta.xml.bind:jakarta.xml.bind-api 下繼續(xù)開發(fā)。務(wù)必確保應(yīng)用程序中所有的導(dǎo)入都已經(jīng)改為了新命名空間jakarta 。例如,對(duì)于 JAXB,將javax.xml.bind.* 改為jakarta.xml.bind.* ,并添加相關(guān)依賴項(xiàng)。下圖中左邊的列是受這項(xiàng)變更影響的模塊。右邊兩列顯示了可以用作依賴項(xiàng)的groupId 和artifactId 。請(qǐng)注意,JAXB 和 JAX-WS 都需要兩個(gè)依賴項(xiàng):一個(gè)用于 API,一個(gè)用于實(shí)現(xiàn)。官方?jīng)]有提供 CORBA 的替代方案,但 Glassfish 還是提供了一個(gè)可用的工件。

Java 15 移除了 JavaScript 引擎 Nashorn,不過(guò),你仍然可以通過(guò)添加以下依賴項(xiàng)來(lái)使用:
<dependency><groupId>org.openjdk.nashorn</groupId><artifactId>nashorn-core</artifactId><version>15.2</version></dependency>
在這個(gè)版本中,JDK 開發(fā)者封裝了一些 JDK 內(nèi)部構(gòu)件。他們不希望應(yīng)用程序再使用 JDK 的底層 API。這主要影響了 Lombok 這樣的工具。所幸,Lombok 幾個(gè)周內(nèi)就發(fā)布了一個(gè)新版本,解決了這個(gè)問(wèn)題。
如果你有任何代碼或依賴項(xiàng)仍然使用 JDK 內(nèi)部構(gòu)件,那么可以嘗試使用 JDK 的高級(jí) API 來(lái)解決這個(gè)問(wèn)題。如果不行的話,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 通過(guò)在pom.xml 文件中指定 JDK 版本來(lái)實(shí)現(xiàn) JDK 切換。很遺憾,當(dāng)使用 Lombok 的舊版本在 Java 16 上運(yùn)行應(yīng)用程序時(shí)報(bào)錯(cuò)了:
[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]上面就是全部報(bào)錯(cuò)信息。我不知道你怎么看,但在我看來(lái),這沒(méi)什么用,所以我提交了這個(gè)問(wèn)題。如果這個(gè)問(wèn)題修復(fù)了,那么使用 Maven Toolchains 切換版本是一種不錯(cuò)的方法。后來(lái),我直接在 Java 16 上運(yùn)行代碼,得到了一個(gè)更具描述性的錯(cuò)誤,其中提到了我之前展示的部分變通方案:
… 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.processingto unnamed module …
JDK 維護(hù)人員已經(jīng)就 9 月份要發(fā)布的內(nèi)容 達(dá)成了一致。Applet API 將被廢棄,因?yàn)闉g覽器停止支持 Applet 已經(jīng)很長(zhǎng)時(shí)間了。實(shí)驗(yàn)性的 AOT 和 JIT 編譯器也將被移除。作為實(shí)驗(yàn)性編譯器的替代方案,你可以使用 GraalVM。最大的變化是 JEP-403:強(qiáng)封裝的 JDK 內(nèi)部構(gòu)件。Java 選項(xiàng)--illegal-access 已經(jīng)無(wú)效,如果你仍然試圖訪問(wèn)一個(gè)內(nèi)部 API,則會(huì)拋出如下異常:
java.lang.reflect.InaccessibleObjectException:Unable to make field private final {type} accessible:module java.base does not "opens {module}" to unnamed module {module}
大多數(shù)時(shí)候,這可以通過(guò)升級(jí)依賴項(xiàng)或使用高級(jí) API 來(lái)解決。如果不行的話,你可以使用--add-opens 參數(shù)來(lái)獲得對(duì)內(nèi)部 API 的訪問(wèn)。不過(guò),除非不得已不要這樣做。注意,有些工具在 Java 17 上還無(wú)法運(yùn)行。例如,Gradle 就無(wú)法構(gòu)建項(xiàng)目,而 Kotlin 不能使用jvmTarget = "17" 。有些框架,如 Mockito,在 Java 17 上也有些小問(wèn)題。enum 字段中的方法會(huì)導(dǎo)致這個(gè)特定的問(wèn)題。不過(guò),我估計(jì)大部分問(wèn)題都會(huì)在 Java 17 發(fā)布之前或發(fā)布之后短期內(nèi)得到解決。對(duì)于任何插件或依賴項(xiàng),你可能會(huì)在構(gòu)建應(yīng)用程序時(shí)看到這條消息“不支持的類文件主版本 61”。類文件主版本 61 用于 Java 17,60 用于 Java 16。這基本上是說(shuō)該插件或依賴項(xiàng)不能用于那個(gè) Java 版本。大多數(shù)時(shí)候,升級(jí)到最新版本就可以解決問(wèn)題。
在解決了所有挑戰(zhàn)之后,你終于可以在 Java 17 上運(yùn)行應(yīng)用程序了。經(jīng)過(guò)努力,你現(xiàn)在可以使用令人興奮的 Java 新特性了,如記錄和模式匹配。
升級(jí) Java 是一項(xiàng)挑戰(zhàn),不過(guò)這也要看你的 Java 版本和依賴項(xiàng)有多老,你的環(huán)境配置有多復(fù)雜。本文旨在幫助你解決 Java 升級(jí)時(shí)最常見的挑戰(zhàn)。一般來(lái)說(shuō),很難評(píng)估實(shí)際的升級(jí)工作要花費(fèi)多長(zhǎng)時(shí)間。我覺(jué)得,大多數(shù)時(shí)候,從 Java 11 升級(jí)到 Java 17 要比從 Java 8 升級(jí)到 Java 11 簡(jiǎn)單。對(duì)于大多數(shù)應(yīng)用程序,從一個(gè) LTS 版本升級(jí)到下一個(gè) LTS 版本需要幾個(gè)小時(shí)到幾天的時(shí)間。大部分時(shí)間都花在了構(gòu)建應(yīng)用程序上。重要的是先開始,然后逐步更改。這樣可以激勵(lì)自己、團(tuán)隊(duì)和管理層繼續(xù)努力。
你開始升級(jí)應(yīng)用程序了嗎?
作者簡(jiǎn)介:
Johan Janssen 是 Sanoma Learning 教育部門的一名軟件架構(gòu)師。他特別喜歡分享 Java 相關(guān)的知識(shí)。他在 Devoxx、Oracle Code One、Devnexus 等會(huì)議上做過(guò)演講。他通過(guò)參與計(jì)劃委員會(huì)來(lái)協(xié)助大會(huì)組織,發(fā)起并組織了 JVMCON。他得過(guò)的獎(jiǎng)項(xiàng)有 JavaOne Rock Star 和 Oracle Code One Star。他在數(shù)字和印刷媒體上撰寫了各種文章。他是 Chocolatey 各種 Java JDK/JRE 包的維護(hù)者,每月有大約 10 萬(wàn)次下載。
原文鏈接:
https://www.infoq.com/articles/why-how-upgrade-java17/
