Share via


從 JAVA 8 轉換至 JAVA 11

將程式碼從 Java 8 轉換成 Java 11 時並沒有一體適用的解決方案。 若是重要的應用程式,從 Java 8 移至 Java 11 時可能會有相當大的工作量。 可能的問題包括已移除的 API、已淘汰的套件、內部 API 的使用、類別載入器的變更,以及記憶體回收的變更。

一般來說,方法是嘗試在 Java 11 上執行而不重新編譯,或先使用 JDK 11 進行編譯。 如果目標是要盡快讓應用程式啟動並執行,那麼嘗試在 Java 11 上執行通常是最好的方法。 針對程式庫,目標是要發佈使用 JDK 11 編譯和測試的成品。

為了移至 Java 11,這些努力是值得的。 自 Java 8 以來,不僅已加入了新功能,也做了許多強化。 這些功能和增強功能改善了啟動、效能、記憶體使用量,並且提高了與容器整合的效能。 此外,API 的新增和修改也提升了開發人員生產力。

本文將說明用來檢查程式碼的工具。 此外也會說明您可能會遇到的問題,以及解決這些問題的建議。 您也應該參閱其他指南,例如 Oracle JDK 移轉指南。 本文並未說明如何將現有程式碼模組化

工具箱

Java 11 有 jdeprscan 和 jdeps 兩種工具,可用來探查可能發生的問題。 這些工具可針對現有的類別或 jar 檔案執行。 您可以直接評估轉換工作,而無須重新編譯。

jdeprscan 會查看是否使用了已淘汰或已移除的 API。 使用已淘汰的 API 並非執行問題,但值得探討。 是否有更新的 jar 檔案? 您是否需要記錄問題,以解決已淘汰 API 的使用問題? 使用已移除的 API 是您嘗試在 Java 11 上執行之前必須先解決的執行問題。

jdeps 是 Java 類別相依性分析器。 與 --jdk-internals 選項搭配使用時,jdeps 可指出您哪個類別相依於哪個內部 API。 您可以繼續在 Java 11 中使用內部 API,但應優先考慮取代這種使用方式。 OpenJDK Wiki 頁面 Java 相依性分析工具針對一些常用的 JDK 內部 API 推薦了適用的替換工具。

Gradle 和 Maven 都有適用的 jdeps 和 jdeprscan 外掛程式。 建議您將這些工具新增至建置指令碼。

Java 編譯器本身 (javac) 是工具箱中的另一項工具。 來自於 jdeprscan 和 jdeps 的警告和錯誤,將會由編譯器發出。 使用 jdeprscan 和 jdeps 的優點是,您可以在現有的 jar 和類別檔案 (包括第三方程式庫) 上執行這些工具。

jdeprscan 和 jdeps 所無法執行的,是針對使用反映來存取封裝 API 的狀況發出警告。 反映存取的檢查會在執行階段進行。 最後,您必須在 Java 11 上執行程式碼才能確知。

使用 jdeprscan

要使用 jdeprscan,最簡單的方式是從現有的組建提供 jar 檔案。 您也可以為其提供目錄 (例如編譯器輸出目錄) 或個別的類別名稱。 使用 --release 11 選項可取得已的淘汰 API 最完整的清單。 如果您想要為已淘汰的 API 排定尋找的優先順序,請將設定切回至 --release 8。 在 Java 8 中已淘汰的 API,可能會比最近淘汰的 API 更快移除。

jdeprscan --release 11 my-application.jar

jdeprscan 工具在無法解析相依類別時,會產生錯誤訊息。 例如: error: cannot find class org/apache/logging/log4j/Logger 。 建議您將相依類別新增至 --class-path,或使用應用程式類別路徑,但即使沒有此路徑,工具仍會繼續掃描。 引數為 --class-path。 類別路徑引數的任何其他變異都不會有作用。

jdeprscan --release 11 --class-path log4j-api-2.13.0.jar my-application.jar
error: cannot find class sun/misc/BASE64Encoder
class com/company/Util uses deprecated method java/lang/Double::<init>(D)V

此輸出指出 com.company.Util 類別正在呼叫 java.lang.Double 類別已淘汰的建構函式。 javadoc 會建議應使用哪個 API 來取代已淘汰的 API。 系統不會執行任何解析 error: cannot find class sun/misc/BASE64Encoder 的工作,因為那是已移除的 API。 自 Java 8 起,應使用 java.util.Base64

執行 jdeprscan --release 11 --list,可了解自 Java 8 以來已淘汰的 API。 若要取得已移除的 API 清單,請執行 jdeprscan --release 11 --list --for-removal

使用 jdeps

使用 jdeps 並搭配 --jdk-internals 選項,可尋找 JDK 內部 API 的相依性。 此範例需使用命令列選項 --multi-release 11,因為 log4j-core-2.13.0.jar多版本 jar 檔案。 若未使用此選項,jdeps 會在找到多版本 jar 檔案時發出警告訊息。 此選項會指定要檢查的類別檔案版本。

jdeps --jdk-internals --multi-release 11 --class-path log4j-core-2.13.0.jar my-application.jar
Util.class -> JDK removed internal API
Util.class -> jdk.base
Util.class -> jdk.unsupported
   com.company.Util        -> sun.misc.BASE64Encoder        JDK internal API (JDK removed internal API)
   com.company.Util        -> sun.misc.Unsafe               JDK internal API (jdk.unsupported)
   com.company.Util        -> sun.nio.ch.Util               JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.misc.Unsafe                          See http://openjdk.java.net/jeps/260   

輸出提供了關於避免使用 JDK 內部 API 的適當建議。 可能的話,建議使用取代 API。 括弧中提供了套件封裝所在的模組名稱。 如果必須明確中斷封裝,模組名稱可以與 --add-exports--add-opens 搭配使用。

使用 sun.misc.BASE64Encoder 或 sun.misc.BASE64Decoder,將導致 Java 11 中出現 java.lang.NoClassDefFoundError。 修改使用這些 API 的程式碼,才能使用 java.util.Base64

請嘗試避免使用任何來自模組 jdk.unsupported 的 API。 此課程模組中的 API 將參考 JDK Enhancement Proposal (JEP) 260 作為建議的替代方案。 簡單地說,JEP 260 指出在有可用的取代 API 之前,可以使用內部 API。 即使您的程式碼可能使用 JDK 內部 API,該 API 至少在一段時間內仍會繼續執行。 請務必查看 JEP 260,因為其中指出了某些內部 API 的替代項目。 例如,變數控制代碼可用來取代某個 sun.misc.Unsafe API。

jdeps 除了可掃描是否使用了 JDK 內部 API 以外,還具備其他功能。 這是一項實用的工具,可用來分析相依性及產生模組資訊檔案。 如需詳細資訊,請參閱文件

使用 javac

使用 JDK 11 進行編譯時,必須更新建置指令碼、工具、測試架構和包含的程式庫。 使用 javac-Xlint:unchecked 選項,可取得 JDK 內部 API 使用情形的詳細資料和其他警告。 此外也可能需要使用 --add-opens--add-reads,以將封裝套件公開至編譯器 (請參閱 JEP 261)。

程式庫可能會將封裝視為多版本 jar 檔案。 多版本 jar 檔案可讓您同時支援來自相同 jar 檔案的 Java 8 和 Java 11 執行階段。 這會增加組建的複雜性。 建置多版本 jar 的方法不在本文件的討論之列。

在 Java 11 上執行

大部分的應用程式都應不需修改即可在 Java 11 上執行。 首要工作是,試著直接在 Java 11 上執行,而不要重新編譯程式碼。 直接執行的目的是要確認執行時會出現哪些警告和錯誤。 此方法可
盡可能減少執行時所需關注的事項,而使應用程式在 Java 11 上更快速地執行。

您可能遇到的問題大多都可逕行解決,而不需要重新編譯程式碼。 如果必須在程式碼中修正問題,請進行修正,但應繼續使用 JDK 8 進行編譯。 可能的話,在使用 JDK 11 進行編譯之前,請讓應用程式使用 java 第 11 版執行

檢查命令列選項

在 Java 11 上執行之前,請執行命令列選項的快速掃描。 已移除的選項會導致 Java 虛擬機器 (JVM) 結束。 如果您使用 GC 記錄選項,這項檢查就特別重要,因為這些選項自 Java 8 起已有大幅變更。 JaCoLine 工具很適合用來偵測命令列選項的問題。

檢查第三方程式庫

您無法控制的第三方程式庫,是潛在的問題來源之一。 您可以主動將第三方程式庫更新為較新的版本。 或者,您可以觀察執行應用程式時未使用的部分,而僅更新必要的程式庫。 將所有程式庫更新為最新版本的問題在於,如果應用程式中發生錯誤,將難以找出根本原因。 是因為某個更新的程式庫而發生錯誤? 還是因為執行階段中的某項變更而導致錯誤? 僅進行必要更新的問題在於,可能需要多次反覆運算才能解決問題。

此處的建議是,進行的變更越少越好,並將第三方程式庫的更新視為個別工作來執行。 如果您更新第三方程式庫,則往往會需要與 Java 11 相容的最新、最高版本。 根據您目前版本落後的程度,您可能需要採取更謹慎的方法,並升級至第一個與 Java 9+ 相容的版本。

除了查看版本資訊以外,您還可以使用 jdeps 和 jdeprscan 來評估 jar 檔案。 此外,OpenJDK 品管小組會維護品質延展 Wiki 頁面,其中列出對 OpenJDK 版本測試眾多免費開放原始碼軟體 (FOSS) 專案的狀態。

明確設定記憶體回收

平行記憶體回收行程 (平行 GC) 是 Java 8 中的預設 GC。 如果應用程式使用預設值,則應使用命令列選項 -XX:+UseParallelGC 明確設定 GC。 Java 9 中的預設值已變更為 Garbage First 記憶體回收行程 (G1GC)。 為了對在 Java 8 與 Java 11 上執行的應用程式進行公平的比較,GC 設定必須相同。 使用 GC 設定進行的實驗,應延遲到應用程式在 Java 11 上經過驗證後再執行。

明確設定預設選項

如果在作用點 VM 上執行,設定命令列選項 -XX:+PrintCommandLineFlags,將會傾印 VM 所設定的選項值,特別是 GC 所設定的預設值。 在 Java 8 上執行時請使用此旗標,而在 Java 11 上執行時請使用列印的選項。 多數情況下,Java 8 到 11 的預設值是相同的。 但使用 Java 8 的設定可確保同位檢查。

建議您設定命令列選項 --illegal-access=warn。 在 Java 11 中,使用反映來存取 JDK 內部 API 會導致不合法的反映存取警告。 根據預設,只有第一次不合法存取時才會發出警告。 設定 --illegal-access=warn 將導致在「每次」進行不合法的反映存取時產生警告。 如果選項設定為「警告」,您將會發現更多不合法存取的案例。 但您也會收到許多多餘的警告。
應用程式在 Java 11 上執行之後,請設定 --illegal-access=deny 以模擬 Java 執行階段未來的行為。 從 Java 16 開始,預設值將是 --illegal-access=deny

ClassLoader 注意事項

在 Java 8 中,您可以將系統類別載入器轉換成 URLClassLoader。 這動作通常會由要在執行階段將類別插入類別路徑中的應用程式和程式庫完成。 在 Java 11 中,類別載入器階層已有所變更。 系統類別載入器 (也稱為應用程式類別載入器) 現在已是內部類別。 轉換成 URLClassLoader 將會在執行階段擲回 ClassCastException。 Java 11 沒有 API 可在執行階段動態擴充類別路徑,但可以透過反映來完成,且此時會清楚顯示使用內部 API 的警告。

在 Java 11 中,開機類別載入器只會載入核心模組。 如果您建立具有 Null 父代的類別載入器,可能會找不到所有平台類別。 在 Java 11 中若遇到這種情形,則必須傳遞 ClassLoader.getPlatformClassLoader() 作為父類別載入器,而不是 null

地區設定資料變更

在 Java 11 中,地區設定資料的預設來源已透過 JEP 252 變更為 Unicode 協會的通用地區設定資料存放庫。 這可能會影響到當地語系化的格式。 如有必要,請設定系統屬性 java.locale.providers=COMPAT,SPI 以還原為 Java 8 地區設定行為。

潛在問題

以下是您可能會遇到的一些常見問題。 如需這些問題的詳細資訊,請遵循連結。

無法辨識的選項

如果命令列選項已移除,應用程式將會列出 Unrecognized option:Unrecognized VM option,且其後尾隨有問題的選項名稱。 無法辨識的選項會導致 VM 結束。 已淘汰但未移除的選項,將會產生 VM 警告

一般來說,已移除的選項並沒有取代項目,唯一的解決方式是從命令列中移除該選項。 記憶體回收記錄的選項屬於例外。 GC 記錄在 Java 9 中會重新實作,以使用整合 JVM 記錄架構。 請參閱 Java SE 11 工具參考的啟用 JVM 整合記錄架構的記錄一節中的「表 2-2:舊版記憶體回收記錄旗標與 Xlog 設定的對應」。

VM 警告

使用已淘汰的選項時,將會產生警告。 選項遭到取代或不再有用時,就會被淘汰。 如同已移除的選項,這些選項應從命令列中移除。 出現 VM Warning: Option <option> was deprecated 警告時,表示選項仍受支援,但未來可能會移除該支援。 已不再支援的選項會產生 VM Warning: Ignoring option 警告。 不再支援的選項不會對執行階段產生影響。

網頁 VM 選項總管提供自 JDK 7 起對 Java 新增或移除的完整選項清單。

錯誤:無法建立 JAVA 虛擬機器

當 JVM 遇到無法辨識的選項時,就會列出此錯誤訊息。

警告:發生不合法的反射存取作業

當 Java 程式碼使用反映來存取 JDK 內部 API 時,執行階段會發出不合法的反映存取警告。

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by my.sample.Main (file:/C:/sample/) to method sun.nio.ch.Util.getTemporaryDirectBuffer(int)
WARNING: Please consider reporting this to the maintainers of com.company.Main
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

這表示模組尚未匯出要透過反映存取的套件。 套件封裝在模組中,且基本上是內部 API。 如果第一項工作是在 Java 11 上啟動並執行,則可以忽略此警告。 Java 11 執行階段允許反映存取,因此舊版程式碼可繼續正常執行。

若要解決此警告,請尋找未使用內部 API 的更新程式碼。 如果無法以更新的程式碼解決此問題,可以使用 --add-exports--add-opens 命令列選項來開放對套件的存取。 這些選項允許從另一個模組存取某個模組的未匯出類型。

--add-exports 選項允許目標模組存取來源模組「公用」類型的具名套件。 有時,程式碼會使用 setAccessible(true) 來存取非公用成員和 API。 這就是所謂的「深度反映」。 在此情況下,請使用 --add-opens 讓您的程式碼存取套件的非公用成員。 如果您不確定應使用 --add-exports 還是 --add-opens,請先使用 --add-exports

您應將 --add-exports--add-opens 選項視為暫時因應措施,而非長期解決方案。 使用這些選項會中斷模組系統的封裝,而使 JDK 內部 API 無法使用。 如果內部 API 已移除或變更,應用程式將會失敗。 在 Java 16 中會拒絕反映存取,除非使用 --add-opens 之類的命令列選項啟用了存取。 若要模擬未來的行為,請在命令列上設定 --illegal-access=deny

由於 java.base 模組未匯出 sun.nio.ch 套件,因此會發出上述範例中的警告。 換句話說,模組 java.basemodule-info.java 檔案中沒有 exports sun.nio.ch;。 這可以透過 --add-exports=java.base/sun.nio.ch=ALL-UNNAMED 來解決。 未在模組中定義的類別,會隱含地屬於「未命名」模組,並以字面方式命名為 ALL-UNNAMED

java.lang.reflect.InaccessibleObjectException

此例外狀況表示您嘗試在封裝類別的欄位或方法上呼叫 setAccessible(true)。 您也可能會收到不合法的反映存取警告。 請使用 --add-opens 選項讓您的程式碼存取套件的非公用成員。 例外狀況訊息會指出模組對於嘗試呼叫 setAccessible 的模組並「未開放」套件。 如果模組是「未命名的模組」,請使用 UNNAMED-MODULE 作為 --add-opens 選項中的目標模組。

java.lang.reflect.InaccessibleObjectException: Unable to make field private final java.util.ArrayList jdk.internal.loader.URLClassPath.loaders accessible: 
module java.base does not "opens jdk.internal.loader" to unnamed module @6442b0a6

$ java --add-opens=java.base/jdk.internal.loader=UNNAMED-MODULE example.Main

java.lang.NoClassDefFoundError

NoClassDefFoundError 最有可能由分割套件造成,或因參考了已移除的模組而造成。

分割套件所造成的 NoClassDefFoundError

分割套件是指一個套件分散於多個程式庫中。 分割套件問題的徵兆是找不到已知位於類別路徑上的類別。

只有在使用模組路徑時,才會發生此問題。 Java 模組系統會將一個套件限定於一個「具名」模組,以最佳化類別查閱。 執行類別查閱時,執行階段會為模組路徑指定高於類別路徑的優先順序。 如果套件在模組與類別路徑之間分割,則只會使用模組來執行類別查閱。 這可能會導致 NoClassDefFound 錯誤。

有個簡單的方法可檢查分割套件,就是將您的模組路徑和類別路徑插入 jdeps 中,並使用應用程式類別檔案的路徑作為 <path>。 如果有分割套件,jdeps 將會列出警告:Warning: split package: <package-name> <module-path> <split-path>

使用 --patch-module <module-name>=<path>[,<path>] 將分割套件新增至具名模組,即可解決此問題。

使用 Java EE 或 CORBA 模組所造成的 NoClassDefFoundError

如果應用程式在 Java 8 上執行,但擲回 java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException,則很可能是因為應用程式使用了 Java EE 或 CORBA 模組中的套件。 這些模組在 Java 9 中已淘汰,且已在 Java 11 中移除

若要解決此問題,請將執行階段相依性新增至您的專案。

已移除的模組 受影響的套件 建議的相依性
適用於 XML Web 服務的 Java API (JAX-WS) java.xml.ws JAX WS RI 執行階段
適用於 XML 繫結的 Java 架構 (JAXB) java.xml.bind JAXB 執行階段
JavaBeans Activation Framework (JAV) java.activation JavaBeans (TM) Activation Framework
一般註釋 java.xml.ws.annotation Javax 註釋 API
通用物件要求訊息代理程式架構 (CORBA) java.corba GlassFish CORBA ORB
Java 交易 API (JTA) java.transaction Java 交易 API

-Xbootclasspath/p 不再是支援的選項

已移除 -Xbootclasspath/p 的支援。 請改用 --patch-module。 --patch-module 選項說明於 JEP 261 中。 尋找標示為「修補模組內容」的區段。 --patch-module 可搭配 javac 和 java 用來覆寫或增強模組中的類別。

--patch-module 實際上會將修補模組插入模組系統的類別查閱中。 模組系統會先從修補模組中擷取類別。 其效用等同於附加在 Java 8 中的 bootclasspath 前面。

UnsupportedClassVersionError

出現此例外狀況時,表示您嘗試在舊版的 Java 上執行以新版 Java 編譯的程式碼。 例如,您在 Java 11 上使用以 JDK 13 編譯的 jar 執行。

Java 版本 類別檔案格式版本
8 52
9 53
10 54
11 55
12 56
13 57

下一步

應用程式在 Java 11 上執行之後,請考慮將程式庫從類別路徑移至模組路徑。 尋找您的應用程式相依之程式庫的更新版本。 選擇模組化程式庫 (如果有的話)。 盡可能使用模組路徑,即使您不打算在應用程式中使用模組也一樣。 使用模組路徑時的類別載入效能優於類別路徑。