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

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

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