從Java 7開始,就一直嚷嚷著要納入的模組化架構,到了Java 8不見推出,這中間又過了好幾年,最後來到了Java 9,而Java 9又因開發不及,從原定的2017年3月改到了2017年7月,如果沒有意外,目前看來,應該是可以如期推出。

而在最新的JDK9預覽版中,也可以玩玩Java模組平臺系統(Java Platform Module System),也就是JSR376的實作了。

JAR檔案的挑戰

撰寫Java的開發者都知道,目前的Java使用JAR檔案來包裝一堆類別檔案,而這些類別檔案會使用套件(package)來加以組織,在編譯或運行Java應用程式時,必須指定類別路徑(CLASSPATH),一開始這不是什麼問題,只是隨著Java應用的場合越來越多,挑戰也就越來越多。

舉例而言,JAR檔案定義在類別路徑中的順序,經常會有同名類別遮蓋的問題;JAR對包裝的類別沒有封裝機制,即使開發中的程式庫想隱藏底層使用到的實作品,然而,只要是public的類型,對客戶端就是可見的;簡而言之,JAR檔案充其量只是個方便傳送的器皿罷了,它沒辦法表示彼此之間的依賴關係,想想看,有多少次發生ClassNotFoundException時,你總是想不透,到底還缺少了哪個JAR檔案呢?

Java SE自己也身受其害。從10MB以下的JDK1.1,到現在200多MB的JDK8,JDK越來越肥大,在錯綜複雜的依賴關係下,有些標準API就算不需要,也必須全盤接受,這對現今的IoT是個挑戰,雖然Java 8之後有了compact profile,可定義三種Java SE的子集,然而,也只是在有限程度下緩解了這個問題。

而且,還有更多其他的問題待解決,這部份可以參考〈Project Jigsaw is Really Coming in Java 9〉(https://goo.gl/15XuYl),某些工具試圖解決部份問題,像是Maven、OSGi等,然而由於一些技術、政治或商業性考量,Java 9終究是採用了Jigsaw來解決(部份的)問題。

類別路徑淡出,模組路徑登場

具體來說,Java 9中的模組仍是使用JAR檔案,這是為了善用現有的工具,以緩解導入或遷移的負擔,一個模組化JAR檔案(modular JAR file)與一般JAR檔案之間的差別,在於根目錄下是否包含一個module-info.class,而它又是由一個module-info.java檔案編譯而來,這個.java是作為模組描述器(Module descriptor)使用。

一個最簡單的模組描述器,可以只包括module cc.openhome.app {}這樣的內容,雖然是個.java檔案,但它不是Java原始碼,開發者不用擔心module成為保留字,而影響了既有的Java原始碼,因為這一個不是Java的.java,以及編譯出來的.class,只是讓編譯器或執行環境能理解這是一個模組。

模組名稱必須是唯一的,與套件名稱類似的,建議使用網域名稱的相反模式,通常是套件名稱的前置部份,如果應用程式依賴在某些模組,在編譯的時候,必須使用--module-path(簡寫-p)或--module-source-path來指定模組路徑,執行時也要指定--module-path,或者是在想要指定起始模組時使用--module(簡寫-m)。

在Java 9推出之後,未來在開發上當然是鼓勵定義模組的,而類別路徑的指定方式,基於向後相容性(Backward compatibility)考量仍舊支援,實際上,一個模組化JAR檔案若是指定於類別路徑中,就會被當成是一般JAR檔案,然而,類別路徑未來應走向逐漸淡出的道路,既有的程式庫在可能的情況下,也要逐漸遷移至新的模組系統。

模組描述器概觀

最簡單的module-info.java,可以只包括module cc.openhome.app {},這樣的模組描述器沒有指定任何依賴模組,這表示它依賴在最基本的java.base模組(若使用到java.sql等其他模組,就會編譯失敗)。Java SE 9將過往有著錯綜複雜依賴關係的標準API,畫分為java.logging、java.sql、java.xml等各個模組,而這些模組最終都依賴在一個java.base模組,它包含了java.lang、java.io等基本套件,而整個Java SE被定義為一個java.se模組。

一個有著依賴關係的模組描述器,會像是:

module cc.openhome.app {
requires cc.openhome.openscad;
requires public cc.openhome.math;
exports cc.openhome.turtle;
}

這部分表示了一些事情——cc.openhome.app模組依賴在cc.openhome.openscad,以及前者的類別對後者匯出(exports)的公開類別,具有讀取性(Readability)。而且,要注意的是,讀取性僅定義了模組的依賴關係圖,例如cc.openhome.app對cc.openhome.openscad是顯式(explicitly)依賴,然而,cc.openhome.openscad可能依賴在其他模組上,因而這些模組也被cc.openhome.app隱式(implicitly)依賴,而對於module cc.openhome.app {},它也隱式依賴在java.base。

只有讀取性,並不代表著可使用被依賴模組中的類別,例如,若有模組requires cc.openhome.app,而上面的模組描述器中並沒有exports cc.openhome.turtle的話(cc.openhome.app中定義的套件),那麼就無法使用cc.openhome.turtle套件中的類別,這表示被public定義的類別不再是全域可見了,定義模組時可以使用exports決定其存取性(Accessibility),因而模組可以實現更強的封裝性(Strong encapsulation)。

有一個問題是,如果有模組requires cc.openhome.app,而cc.openhome.openscad模組中,傳回了cc.openhome.math套件中的類別類型,那麼,最前頭的模組可以也使用該類型嗎?

對此,簡單的想法是,讓開發者自行在一開始的模組描述器,也加上requires cc.openhome.openscad,不過,這作法麻煩也不可靠,如果cc.openhome.app願意的話,只要加上requires public cc.openhome.math,就可以解決這個問題。

相容性與遷移

無疑地,既有的應用程式,還是要能在Java 9上運行的,其中一個作法如前所述,一個模組化JAR檔案若是指定於類別路徑中,會被當成是一般JAR檔案,實際上,置於類別路徑中的類別,都會被歸類在一個未命名模組(unnamed module),未命名模組對任何模組都具有讀取性,對被模組匯出的套件中的任何類別具有存取性。

而在既有程式庫的遷移上,有一個簡單的情境是,如果某個JAR檔案未依賴類別路徑上的其他程式庫,而且經檢查僅依賴在java.base模組,那麼,這個JAR檔案可以進一步定義為模組化檔案;接著,若另一個JAR檔案依賴在這個JAR,就可以定義其依賴在新定義的模組,以及其他的java.sql等模組上,依此類推、完成遷移。整體而言,這是一種自底而上的遷移(Bottom-up migration)方式。

當然,實際的依賴情況可能更複雜,可以使用jdeps之類的工具來檢查相依關係,而開發者也需要瞭解更多模組化的細節,此時,Simon Ritter的演講〈Project Jigsaw in JDK 9: Modularity Comes To Java〉(https://goo.gl/3Scf6n)會是個好的開始。

在這之後,OpenJDK官方的〈The State of the Module System〉(https://goo.gl/HFN9Em)是個詳盡的文件。

專欄作者

熱門新聞

Advertisement