從JDK8升級到JDK11,看這篇就足夠了

原文地址:https://blog.codefx.org/java/java-11-migration-guide/。 在原文的基礎上,增加了一些我遇到的具體的坑還有在特定場景下的解決方案,供大家參考

一些背景

在背景知識,我們會討論一些關於新的JDK Release週期,OpenJDK特性歸一化,LTS(Long-term support長期支持版本)的事情。

1. 新的發佈週期

這個就可以長話短說了,反正我們知道如下兩點就好:

  • 每六個月發佈一個大更新(就是每年的3月還有9月)
  • 對於每個大版本更新,會有兩次小版本更新(在發佈後一個月或者四個月之後)

2. OpenJDK已可以作爲新的線上標準JDK

在2018.9之前,Oracle JDK是大家普遍運用於線上的JDK,OpenJDK的特性並不完全,並且Oracle JDK號稱做了很多優化。在2018.9之後,Oracle JDK正式商用(開發不收費,但是運行線上業務收費)。但是與此同時,Oracle宣佈,OpenJDK與Oracle JDK在功能上不會有區別。並且,OpenJDK 11 RTS將會由紅帽社區進行維護。這樣,更加增加了可靠性與保證問題的及時解決。

我們可以在線上使用OpenJDK,開發時,使用任意的JDK。

3. LTS(Long-term support長期維護)版本

對於商業版的JDK,不同的廠商都將長期維護版本定在JDK 11/17/23/…

對於OpenJDK,社區說,對於這些版本,至少會提供四年的維護更新時間。每個長期維護版本都會有一個固定的管理者,對於OpenJDK11,應該就是紅帽社區。現在源代碼搞定了,但是,我們應該從哪裏獲取編譯好的OpenJDK呢?這個可以交給AdoptOpenJDK,它會一直收集不同版本的OpenJDK以及全平臺的build好的OpenJDK

4. Amazon Corretto

AWS也提供了自己的OpenJDK,Amazon Corretto:

  • 基於OpenJDK,採取GPL+CE協議,做了一些安全性,性能和穩定性優化,並且修復了一些bug
  • 支持linux,MAC OS還有Windows操作系統
  • 長期支持Java 8並且至少到2023年
  • 從2019年開始支持Java 11並且至少到2024
  • 季度更新,並且伴隨一些緊急bug修復的更新

OpenJDK社區的FAQ部分曾經提到:“Amazon從2017年開始貢獻OpenJDK,並且計劃開始大量貢獻”。我猜Amazon會把他們在Corretto上面做的優化,合併到OpenJDK源碼中,即使沒有,Corretto也是開源的,遲早會有人蔘考並在OpenJDK源碼上進行修改。同時也說明,OpenJDK的更新也會及時被合併到Corretto中。

準備遷移

1. 更新好開發環境以及編譯環境

各種常用工具,建議升級到如下版本以後:

  • IntelliJ IDEA: 2018.2
  • Eclipse: Photon 4.9RC2 with Java 11 plugin
  • Maven: 3.5.0
  • Maven compiler plugin: 3.8.0
  • surefire and failsafe: 2.22.0
  • Gradle: 5.0

對於如下工具,由於已經不再維護,需要替換成其他工具:

  • FindBugs(靜態代碼bug發現): 用SpotBugs替換。
  • Cobertura(代碼測試覆蓋率):用Jacoco替換

同時由於在Java 9 之後,每六個月bytecode level會提升一次。如果你依賴的庫有處理字節碼相關的庫,應該注意下版本升級,例如:

  • 對於直接操作字節碼的庫,如果你升級了JDK,那麼最好也跟着升級這些庫:ASM (7.0), Byte Buddy (1.9.0), cglib (3.2.8), or Javassist (3.23.1-GA).這些版本是OpenJDK11適用的版本
  • 如果你使用的庫依賴了上面提到的操作字節碼的庫,那麼也需要注意下版本依賴,看依賴的操作字節碼的庫是否升級到了上面提到的版本。對於Spring,最好採用5.1以後的版本Mockito則是2.20.0以後的版本

2. 引入JPMS後,相關的遷移工作

2.1. Java EE相關模塊默認不在Java包裏面了,相關的類需要增加額外依賴或者替換成其他的類

如果你的項目中使用了這些類,那麼在編譯階段就會報錯,例如:

error: package javax.xml.bind does not exist
import javax.xml.bind.JAXBException;
                     ^

如果你是用JDK 8編譯成功,拿到JDK 11運行,就會報錯:

Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/JAXBException
    at monitor.Main.main(Main.java:27)
Caused by: java.lang.ClassNotFoundException: javax.xml.bind.JAXBException
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)
    ... 1 more

以下是相關移除列表還有解決方案

  1. JavaBeans Activation Framework (JAF) (javax.activation)變成了一個獨立的框架,maven依賴:
<dependency>
    <groupId>com.sun.activation</groupId>
    <artifactId>javax.activation</artifactId>
    <version>1.2.0</version>
</dependency>
  1. CORBA(java.corba)在JEP 230已經不復存在了,在你的項目中如果遇到,證明你的項目太古老了。移除掉吧
  2. JTA (java.transaction)變成了獨立依賴:
<dependency>
    <groupId>javax.transaction</groupId>
    <artifactId>javax.transaction-api</artifactId>
    <version>1.2</version>
</dependency>
  1. JAXB和JAX-WS:
<dependency>
    <groupId>javax.xml.bind</groupId>
    <artifactId>jaxb-api</artifactId>
    <version>2.2.8</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-core</artifactId>
    <version>2.2.8</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.bind</groupId>
    <artifactId>jaxb-impl</artifactId>
    <version>2.2.8</version>
</dependency>
<dependency>
    <groupId>com.sun.xml.ws</groupId>
    <artifactId>jaxws-ri</artifactId>
    <version>2.3.0</version>
    <type>pom</type>
</dependency>
  1. Common Annotations:
<dependency>
    <groupId>javax.annotation</groupId>
    <artifactId>javax.annotation-api</artifactId>
    <version>1.3.1</version>
</dependency>

一個建議就是,在你的項目中如果沒有衝突,建議都加上這些依賴。

2.2. 模塊可見性導致的內部API不能調用的問題

這個在我另一篇文章也說過:https://zhanghaoxin.blog.csdn.net/article/details/90514045

在Java9之後引入了模塊化的概念,是將類型和資源封裝在模塊中,並僅導出其他模塊要訪問其公共類型的軟件包。如果模塊中的軟件包未導出或打開,則表示模塊的設計人員無意在模塊外部使用這些軟件包。 這樣的包可能會被修改或甚至從模塊中刪除,無需任何通知。 如果仍然使用這些軟件包通過使用命令行選項導出或打開它們,可能會面臨破壞應用程序的風險!

對於這種限制,在編譯階段,可能會有類似下面的報錯:


error: package com.sun.imageio.plugins.jpeg is not visible
import com.sun.imageio.plugins.jpeg.JPEG;
                              ^
  (package com.sun.imageio.plugins.jpeg is declared
  in module java.desktop, which does not export it)

如果是反射的調用,可能在運行階段有類似於如下的報警:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by j9ms.internal.JPEG
    (file:...) to field com.sun.imageio.plugins.jpeg.JPEG.TEM
WARNING: Please consider reporting this
    to the maintainers of j9ms.internal.JPEG
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
# here's the reflective access to the static field com.sun.imageio.plugins.jpeg.JPEG.TEM

對於這種錯誤,我們最好是更換API,如果難以實現,則可以通過添加編譯以及啓動參數解決。

我們需要的參數是:

  • --add-exports選項:模塊聲明中的exports語句將模塊中的包導出到所有或其他模塊,因此這些模塊可以使用該包中的公共API。 如果程序包未由模塊導出,則可以使用-add-exports的命令行選項導出程序包:
--add-exports <source-module>/<package>=<target-module-list>

如果設置target-module-list爲ALL-UNNAMED,那麼所有Classpath下的module,都可以訪問source-module中的pakage包下的公共API

  • --add-opens選項: 模塊聲明中的opens語句使模塊裏面的包對其他模塊開放,因此這些模塊可以在運行期使用深層反射訪問該程序包中的所有成員類型。 如果一個模塊的包未打開,可以使用–add-opens命令行選項打開它。 其語法如下:
--add-opens <source-module>/<package>=<target-module-list>

如果設置target-module-list爲ALL-UNNAMED,那麼所有Classpath下的module,都可以訪問source-module中的pakage包下的所有成員類型

對於編譯階段,也就是javac命令,我們只需要添加--add-exports,對於上面的例子,就是:

javac --add-exports java.desktop/com.sun.imageio.plugins.jpeg=ALL-UNNAMED

對於運行階段,也就是java命令,我們最好把--add-exports--add-open都加上,對於上面的例子,就是:

java --add-exports java.desktop/com.sun.imageio.plugins.jpeg=ALL-UNNAMED --add-open java.desktop/com.sun.imageio.plugins.jpeg=ALL-UNNAMED

這樣,在運行階段,首先不會有禁止訪問報錯,同時也不會有警告。

同時,爲了在運行期能找到所有需要添加的模塊和包,可以通過添加--illegal-access=${value}來檢查。這個value可以填寫:

  • permit: 未來可能會移除。僅在第一次反射調用內部api的時候報警
  • warn:每次次反射調用內部api的時候報警
  • debug:在warn的基礎上,加上堆棧輸出
  • deny: 拒絕所有非法反射訪問內部api

我們可以設置--illegal-access=deny來知道我們需要添加的所有--add-export--add-open包。

2.3 通過JDK11內置jdeps工具查找過期以及廢棄API以及對應的替換

這個也在我另一篇文章提到過:
https://zhanghaoxin.blog.csdn.net/article/details/100732605


jdeps --jdk-internals -R --class-path 'libs/*' $project

libs是你的所有依賴的目錄,$project是你的項目jar包,示例輸出:

...
JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.misc.BASE64Encoder                   Use java.util.Base64 @since 1.8
sun.reflect.Reflection                   Use java.lang.StackWalker @since 9

在這裏簡單提一些在JDK11過期,但是JDK8使用的API:

  • sun.misc.Base64 (替換成 java.util.Base64)
  • com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel (替換成 javax.swing.plaf.nimbus.NimbusLookAndFeel)
  • java.util.LogManager, java.util.jar.Pack200.Packer類 Unpacker: addPropertyChangeListener 和 removePropertyChangeListener這兩個方法已經移除
  • java.lang.Runtime類: methods getLocalizedInputStream 和 getLocalizedOutputStream方法已經移除
  • SecurityManager的操作方法已經整體移除

2.4. ClassLoader變化帶來的URLClassLoader的變化

Java 8的ClassLoader流程:

  • bootstrap classloader加載rt.jar,jre/lib/endorsed
  • ext classloader加載jre/lib/ext
  • application classloader加載-cp指定的類

java9及之後的classloader流程:

  • bootstrap classloader加載lib/modules
  • ext classloader更名爲platform classloader,加載lib/modules
  • application classloader加載-cp,-mp指定的類

同時,我們注意到,JDK9開始,AppClassLoader他爹不再是 URLClassLoader

一般熱部署,插件部署,都會使用到AppClassLoader,例如Spring-Boot的熱部署,老版本的會報異常:

Exception in thread "main" java.lang.ClassCastException: java.base/jdk.internal.loader.ClassLoaders$AppClassLoader cannot be cast to java.base/java.net.URLClassLoader
    at org.springframework.boot.devtools.restart.DefaultRestartInitializer.getUrls(DefaultRestartInitializer.java:93)
    at org.springframework.boot.devtools.restart.DefaultRestartInitializer.getInitialUrls(DefaultRestartInitializer.java:56)
    at org.springframework.boot.devtools.restart.Restarter.<init>(Restarter.java:140)
    at org.springframework.boot.devtools.restart.Restarter.initialize(Restarter.java:546)
    at org.springframework.boot.devtools.restart.RestartApplicationListener.onApplicationStartingEvent(RestartApplicationListener.java:67)
    at org.springframework.boot.devtools.restart.RestartApplicationListener.onApplicationEvent(RestartApplicationListener.java:45)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
    at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:122)
    at org.springframework.boot.context.event.EventPublishingRunListener.starting(EventPublishingRunListener.java:69)
    at org.springframework.boot.SpringApplicationRunListeners.starting(SpringApplicationRunListeners.java:48)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:292)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1118)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1107)
    at com.asofdate.AsofdateMain.main(AsofdateMain.java:18)

這是主要是因爲AppClassLoader不再是URLClassLoader的子類導致的。

之前對於動態加載的類,我們總是通過將這個類通過反射調用URLClassLoader加到classpath裏面進行加載。這麼加載在JDK11中已經無法實現,並且這樣加載的類不能卸載。
對於動態加載的類,我們在OpenJDK11中只能自定義類加載器去加載,而不是通過獲取APPClassLoader去加載。同時,這麼做也有助於你隨時能將動態加載的類卸載,因爲並沒有加載到APPClassLoader。

建議使用自定義的類加載器繼承SecureClassLoader去加載類:
java.security.SecureClassLoader

最後,如果你想訪問classpath下的內容,你可以讀取環境變量:

String pathSeparator = System
    .getProperty("path.separator");
String[] classPathEntries = System
    .getProperty("java.class.path")
    .split(pathSeparator);

2.5. 過期啓動參數修改

JDK 8 到JDK 11有很多參數變化,可以總結爲兩類參數的變化,一是GC相關的(GC配置調優更加簡單),二是日誌相關的,日誌統一到了一起,不像之前那麼混亂

具體請參考:

  1. https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-4856361B-8BFD-4964-AE84-121F5F6CF111
  2. https://docs.oracle.com/javase/9/tools/java.htm#JSWOR-GUID-4856361B-8BFD-4964-AE84-121F5F6CF111
  3. https://docs.oracle.com/javase/10/tools/java.htm#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE__REMOVEDJAVAOPTIONS-A4E6F213

每個說明參考三部分:

  1. Obsolete Java Options: 參數可以被接受但是忽略掉了,但是會有警告,一般這種參數有替代寫法,請用替代的寫法
  2. Deprecated Java Options: 參數可以被接受並有效,但是會有警告,一般這種參數有替代寫法,請用替代的寫法
  3. Removed Java Options: 參數被移除,並且使用的話會有報錯

3. 一些框架的OpenJDK11兼容問題持續收集(持續更新中)

  1. OpenJDK11與Spring Cloud Finchley的不兼容問題與解決: https://blog.csdn.net/zhxdick/article/details/102314886
  2. Lombok編譯異常: 升級到1.18.+的版本纔可以
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章