在閱讀了一些的文件,也對模組化做了不少認識之後,下一步可能會想,試著來把某個專案放到JDK9上跑跑看吧!可能的話,進一步玩玩模組化,不過要從 何開始呢?套句現代的玩笑話,如果人品不好的話,你連IDE都開不起來!
- 解決基於JDK9的問題
正如先前專欄〈Hello, JDK9?〉中談到的,在Java 9中,JDK/JRE重新架構了,不再有rt.jar、tools.jar等檔案,而有了JMOD與JIMAGE,IDE之類的工具若依賴在這類資訊上,工具本身必須更新 才能在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.URLClassLoade
r的實例,然而,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〉示範了自底而上的遷移方式可供參考。
如果有些程式庫並不在專案控制之下,而且該程式庫還沒有(或將來也不會)模組化,將之置於類別路徑成為未命名模組會有許多不便之處,這時可以將之置於 模組路徑,使之成為自動模組,〈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〉中示範了方才談到的這個過程。
那麼自動模組的名稱從哪來的?如果既有的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〉;在決定自己的應用程式是否遷移至模組化之前,看看使用到的第三方程式庫,官方是不是都決定好(自
動)模組名稱了,可以免去後續還得修改模組名稱的麻煩。- 黑魔法的用與不用!
在遷移至Java 9,甚至是進往模組化的路上,會遇到的問題實際上還不只這些,有興趣還可以參考〈Java 9 Migration Guide: The Seven Most Common Challenges〉。
活用
-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〉。不過,黑魔法終究只是權宜之計,畢竟非標準屬性並不保證未來版本還會存在,而且就算想使用黑魔法,還是得知道許多模組細節才能得心應手,系統性且深入 地認識模組化是必要的,這部份的話,可以參考《Java 9 Revealed》或者是《Java 9 Modularity》,想接觸更多遷移到JDK9時會遇上的怪問題?「WTF, Java 9?!」會是你想要的!