在正如先前專欄〈Hello, JDK9?〉中談到,在Java 9中,JDK/JRE重新架構,不再有rt.jar、tools.jar等檔案,而有了JMOD與JIMAGE,IDE之類的工具若依賴在這類資訊,工具須更新,才能在JDK9運行,然後,準備面對一大堆編譯錯誤!

解決基於JDK9的問題

既有的專案一開始基於類別路徑而運行時,正如〈當拼圖遇上反射〉提過,為了相容性,類別路徑上的類別,都會被歸入未具名模組,這時,若使用的是java.sql、java.util.logging套件中的API,基本上沒問題,然而若使用到javax.xml.bind.*、javax.rmi等套件,就會出現編譯錯誤,這是因為,上述套件雖然包含在Java SE,然而,實際上是與Java EE相關的API——在JDK9中是有這些套件,卻是被畫分到java.se.ee模組,因此編譯與執行時,必須使用--add-modules java.se.ee,模組圖中才找得到這些套件。

同樣可能是因為找不到套件,而導致編譯錯誤的另一個情況,則是:既有程式使用JDK內部的非標準API,像是sun.*套件或子套件下的類別,雖然未具名模組可讀取所有模組,但能否使用模組中的API,還是要看模組有無exports,而java.base模組的sun.*套件或子套件,並沒有exports,因此發生編譯錯誤。

修改java.base的module-info,顯然不可行。Oracle JDK中提供非標準引數--add-exports,可以用來放寬(或說是破壞)模組封裝。例如:可以在編譯或執行時期,加上--add-exports java.base/sun.net.ftp=ALL-UNNAMED,這就可以將java.base模組的sun.net.ftp匯出給未具名模組;如果有多個套件必須exports,編譯或執行時,可以指定多個--add-exports。

如果專案運用了反射,或者類別載入器,記得,JDK9的類別載入器也有了變動,這可能會影響類別是由哪個載入器載入的判斷。另一方面,過去版本的JDK中,Extended與System載入器是java.net.URLClassLoader的實例,然而,到了JDK9,Platform與System載入器是JDK內部定義的類別,如果既有的專案依賴URLClassLoader型態,來操作Extended與System載入器,在JDK9就會發生錯誤。

另一個延伸的問題是,既然類別不再只是來自JAR檔案,那麼,像ClassLoader的getSystemResource、getResource等方法,還是使用jar:file:$javahome/lib/rt.jar!$path這樣的URL嗎?為了因應模組化,以及有JAR、JMOD與JIMAGE的選擇,現在要取得系統資源時,URL會是像jrt:/$module/$path的形式。

既有JAR成為自動模組

如果既有的專案,可以在JDK9執行,下一步,就是試著朝模組化前進。若專案沒有依賴第三方程式庫,既有程式庫都是自行開發且沒有複雜依賴關係時,會是最簡單的情況,〈The State of the Module System〉的〈Bottom-up migration〉(https://goo.gl/ZSjfWv)示範自底而上的遷移,可供參考。

如果有些程式庫並不在專案控制之下,而且,該程式庫還沒有(或將來也不會)模組化,將之置於類別路徑成為未命名模組,會有許多不便之處,這時,可以將之置於模組路徑,使之成為自動模組,〈The State of the Module System〉的〈Automatic modules〉,說明了這種情況下的遷移,自動模組將成為未命名模組至顯式模組間的橋樑。

然而,情況往往不會那麼順利。如果有一個套件的API,分散在兩個JAR,當這兩個JAR都置於模組路徑中,就會發生找不到套件或類別的編譯錯誤。

因為,在模組化的規範,一個套件不能存在兩個模組,這種情況若發生,該套件被視為分裂套件(Split Package)。而且,若發現分裂套件,模組路徑較後頭的JAR會被忽略,也就找不到套件或類別。

如果可以自行控制這些套件,最好的方式,是將兩個JAR中的分裂套件,合併為同一個JAR。然而,若做不到這點,另一個方式,就是在編譯或執行時,透過非標準屬性--patch-module來修補模組,例如:在cc.openhome.jar與cc.openhome.abc.jar中,包含了同一套件,這時,可以使用--patch-module cc.openhome=path/to/cc.openhome.abc.jar,將後者包含的套件、類別等,加入cc.openhome模組,這時,模組路徑中就不用包含cc.openhome.abc.jar了。

定義顯式模組

接下來,可能會開始對一些置於模組路徑的專案,進行模組化,而不是讓它成為自動模組。該模組可能會依賴在其他模組,因此必須知道依賴的模組名稱。方式之一,是透過JDK的jdeps工具程式,它也可以協助找出分裂套件。

有了模組名稱,就知道在module-info定義哪些requires,而一旦定義顯式模組,就不再是自動exports全部套件,若因此使得專案產生編譯錯誤,記得在目前定義的顯式模組,加上必要的exports。

如果需要實際的案例,在〈Painlessly Migrating to Java Jigsaw Modules - a Case Study〉(https://goo.gl/4TXn4N)中,示範了方才談到的這個過程。

那麼,自動模組的名稱從哪來的?如果既有的JAR沒有任何進一步更新的話,自動模組會從JAR檔案名稱,試著擷取出模組名稱。

例如,若是cc.openhome-1.0.jar,先取得主檔名,再去除版本號區段,版本號須是連字號(-)或底線(_)後跟隨數字,然後將名稱中非字母部份,替換為句號(.),因此得到cc.openhome這個模組名稱。

在這樣的規則之下,不見得每個JAR都可以正確產生模組名稱。例如:cc.openhome.util_1.0-spec-1.0.jar就會失敗,編譯時期就沒有名稱可以requires,而執行時期模組路徑上存在這種JAR的話,就會產生IllegalArgumentException,從而使得JVM無法初始模組層而發生FindException。

若不想基於檔名決定自動模組名稱,在既有的JAR中,可以在META-INF/MANIFEST.MF裡,增加Automatic-Module-Name,指定自動模組名稱。然而,對於第三方程式庫的既有JAR,不建議自己做這個動作,最好是讓第三方程式庫的釋出者決定自動模組名稱,免得以後產生名稱上的困擾。

但問題就在這邊了,若第三方程式庫官方還沒決定模組化,或者決定自動模組名稱之前,依檔名來產生模組名稱,並於定義顯式模組時requires,實際上也會產生困擾。可參考〈Java SE 9 - JPMS automatic modules〉(https://goo.gl/epF53r)。

因此,在決定自己的應用程式是否遷移至模組化之前,看看使用到的第三方程式庫,確認官方是否都決定好(自動)模組名稱了,可以免去後續還得修改模組名稱的麻煩。

黑魔法的用與不用!

在遷移至Java 9,甚至是進往模組化的路上,會遇到的問題還不只這些,可參考〈Java 9 Migration Guide: The Seven Most Common Challenges〉(https://goo.gl/Wn47uH)。

而活用-classpath、--module-path、--add-modules等標準屬性,甚至是--add-exports非標準屬性,可以暫時性地解決問題。

除了--add-exports之外,還有--add-opens可以放寬(破壞)模組封裝,如果你的專案在深層反射遇上的問題,就可以試試看。另外,也有--add-reads非標準屬性,可以臨時在目前模組增加requires的模組。前面談到的非標準屬性--patch-module,也是黑魔法的一種,有興趣看看這些黑魔法的應用,可以參考〈Five Command Line Options To Hack The Java 9 Module System〉(https://goo.gl/U5P4tV)。

不過,黑魔法終究只是權宜之計,畢竟非標準屬性並不保證未來版本還會存在,而且,就算想使用黑魔法,還是得知道許多模組細節,才能得心應手,所以,系統性且深入地認識模組化是必要的。

而這能參考《Java 9 Revealed》或《Java 9 Modularity》,想接觸更多遷移JDK9的怪問題?「WTF, Java 9?!」(http://java9.wtf/)是你想要的!

作者簡介


Advertisement

更多 iThome相關內容