共用方式為


從 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 有兩個工具 jdeprscanjdeps,可用於探查潛在問題。 這些工具可以針對現有的類別或 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 都有 jdepsjdeprscan 外掛程式。 建議您將這些工具新增至組建腳本。

Java 編譯程式本身 javac 是工具箱中的另一個工具。 您從 jdeprscanjdeps 取得的警告和錯誤將會來自編譯程式。 使用 jdeprscanjdeps 的優點是您可以透過現有的 jar 和類別檔案來執行這些工具,包括第三方連結庫。

jdeprscanjdeps 無法警告使用反射來存取封裝 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。 由於 API 已被移除,再多的努力也無法解決 error: cannot find class sun/misc/BASE64Encoder 的問題。 從 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.BASE64Encodersun.misc.BASE64Decoder 會導致 Java 11 中的 java.lang.NoClassDefFoundError 。 使用這些 API 的程式代碼必須修改為使用 java.util.Base64

請嘗試排除使用來自 模組 jdk.unsupported 的任何 API。 本模組的 API 將參考 JDK 增強提案 (JEP) 260,作為建議的替代方案。 簡言之,JEP 260 表示支援使用內部 API,直到有替代 API 可用為止。 雖然您的程式代碼可能使用 JDK 內部 API,但至少會持續執行一段時間。 請務必查看 JEP 260,因為它確實指出一些內部 API 的替代方案。 例如,變數句柄可用來取代某些 sun.misc.Unsafe API。

jdeps 不僅僅是用來掃描 JDK 內部的使用情況。 這是用來分析相依性和產生模組資訊檔案的實用工具。 如需詳細資訊,請參閱

使用 javac

使用 JDK 11 進行編譯需要更新,才能建置腳本、工具、測試架構和包含的連結庫。 請使用 -Xlint:unchecked 選項搭配 javac 來獲得使用 JDK 內部 API 和其他警告的詳細資訊。 您也可以使用 --add-opens--add-reads 將封裝的套件公開給編譯程式 (請參閱 JEP 261)。

程式庫可以考慮封裝為 多版本 jar 檔案。 多版本 jar 檔案可讓您從相同的 jar 檔案支援 Java 8 和 Java 11 執行環境。 它們確實會增加構建的複雜性。 如何建置多版本的 Java JAR 檔案不在本文範疇內。

在 Java 11 上執行

大部分的應用程式都應該在 Java 11 上執行,而不需修改。 嘗試的第一件事是在 Java 11 上執行,而不需重新編譯程式代碼。 僅僅跑程序的目的是查看執行過程中出現哪些警告和錯誤。 此方法會獲得一個結果
應用程式以更快速地在 Java 11 上運行,透過集中於需完成的最小必要的事情。

您可能會遇到的大部分問題都可以解決,而不需要重新編譯程序代碼。 如果程式代碼中必須修正問題,請進行修正,但繼續使用 JDK 8 進行編譯。 如果可能,請先將應用程式執行至第 11 版java,然後再用 JDK 11 編譯

檢查命令列選項

在 Java 11 上執行之前,請先快速掃描命令行選項。 已移除的選項 會導致 Java 虛擬機 (JVM) 結束。 如果您使用 GC 記錄選項,這項檢查就特別重要,因為自 Java 8 起它們已經發生了重大變化。 JaCoLine 工具是用來偵測命令行選項問題的好工具。

檢查第三方函式庫

潛在的問題來源是您無法控制的第三方連結庫。 您可以主動將第三方連結庫更新為較新版本。 或者,您可以查看執行應用程式的結果,並只更新必要的函式庫。 將所有連結庫更新為最新版本的問題在於,如果應用程式中發生一些錯誤,就很難找到根本原因。 錯誤是否因為某些更新的函式庫而發生? 還是運行時發生某些變更所造成的錯誤? 只更新必要的部分的問題在於可能需要數次反覆才能解決。

這裡的建議是盡可能少進行變更,並將更新第三方庫作為單獨的工作進行。 如果您更新第三方程式庫,您往往會希望使用與 Java 11 相容的最新且功能最完善的版本。 根據目前版本落後多少,您可能想要採取更謹慎的方法,並升級至第一個 Java 9+ 相容版本。

除了查看版本資訊之外,您還可以使用 jdepsjdeprscan 來評估 jar 檔案。 此外,OpenJDK 品質群組會維護 品質外展 Wiki頁面,其中列出針對OpenJDK版本測試許多免費開放原始碼軟體 (FOSS) 項目的狀態。

明確設定垃圾收集

平行垃圾收集器 (Parallel GC) 是 Java 8 中的預設 GC。 如果應用程式使用預設值,則應該使用命令行選項 -XX:+UseParallelGC明確設定 GC。 Java 9 的預設值已變更為 Garbage First 垃圾回收器(G1GC)。 為了公平比較在 Java 8 與 Java 11 上執行的應用程式,GC 設定必須相同。 試驗 GC 設定應該延後,直到應用程式在 Java 11 上獲得驗證為止。

明確設定預設選項

如果在 HotSpot VM 上執行,設定命令行選項 -XX:+PrintCommandLineFlags 將會傾印 VM 所設定的選項值,特別是 GC 所設定的預設值。 在 Java 8 上使用這個旗標執行,並在 Java 11 上執行時使用列印選項。 在大多數情況下,預設值與 8 到 11 相同。 但是,使用第 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。 這通常是由應用程式和函式庫在執行時想要將類別插入 classpath 所完成的。 類別載入器階層已在 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。 不再支援的選項不會影響運行時間。

提供自 JDK 7 起新增至或從 Java 移除的詳盡選項清單的網頁為 VM 選項瀏覽器

錯誤:無法建立 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

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

java.lang.reflect.InaccessibleObjectException

這個例外狀況表示您嘗試在封裝類別的欄位或方法上呼叫 setAccessible(true) 。 您也可能會收到 非法反射存取警告。 使用--add-opens選項,讓您的程式碼可以存取套件的非公開成員。 例外狀況訊息會告訴您,模組「未將」封裝開放給嘗試呼叫 setAccessible 的模組。 如果模組為「未命名的模組」,請使用 UNNAMED-MODULE 作為 --add-opens 選項中的 target-module。

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 ,並使用應用程式類別檔案的路徑作為 <路徑>。 如果有分割套件,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) 啟用架構
一般註釋 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 可與 javacjava 搭配使用,以覆寫或增強模組中的類別。

--patch-module 實際上的作用是將補丁模組插入模組系統的類別查找。 模組系統會先從修補程式模組擷取 類別。 這與在 Java 8 中將啟動類別路徑預置的效果相同。

UnsupportedClassVersionError(不支援的類別版本錯誤)

這個例外狀況表示您嘗試在舊版 Java 上執行以更新版本的 Java 編譯的程式代碼。 例如,您在使用 Java 11 執行以 JDK 13 編譯的 jar。

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

後續步驟

應用程式在 Java 11 上執行之後,請考慮將連結庫從類別路徑移至模組路徑。 尋找您的應用程式所依賴函式庫的更新版本。 如果有的話,請選擇模組化連結庫。 盡可能使用模組路徑,即使您不打算在應用程式中使用模組也一樣。 使用模組路徑在類別載入方面具有比類別路徑更好的效能。