當Java 9拼圖(Jigsaw)支援模組化,除了JDK本身檔案重新布局之外,另一個問題就是反射(Reflection)這個黑魔法。

模組的目的是為了強封裝,而反射正與其背道而馳,如果應用程式重度依賴在反射,或者使用了重度依賴反射的程式庫或框架,在遷移至Java 9之前,可得先搞清楚兩者之間的關係。

模組圖

在Java 9之前,若沒有受到SecurityManager的限制,只要類別可在類別路徑(Class path)中找到,就可以使用反射獲取Class實例,從中取得類別資訊、操作公開方法,或甚至設定AccessibleObject的setAccessible(true)來存取私有值域或操作私有方法。對於這是否破壞了封裝性,在Java界一向就是個爭議點,然而不容否認的事實是,許多程式庫或框架正是依賴在這種反射機制上,才得以運作。

既然Java 9拼圖是為了強封裝,因而立即面臨的挑戰之一,是在反射機制上取得平衡,就結論而言,就是讓模組的設計者可以決定是否允許反射。

首先,當一個模組置於模組路徑(Module path)中,只是表示JVM可以找到模組定義(module-info.class),這時,還不能使用反射來取得該模組中的類別(會有ClassNotFoundException),因為模組圖(Module graph)中並不存在該模組,應用程式也就不具有讀取能力(Readability)。

要將模組加入模組圖,方式之一,是執行java指令啟動JVM時,使用--add-modules引數加入為根模組,另一個方式,則是在目前模組定義中加入requires設定,這時現有模組會依賴在被requires的模組之上,這時,可以取得被requires的模組中之類別Class實例,進行基本資訊的「讀取」,就算被requires的模組並沒有exports任何套件。

然而,如果你想進一步運用反射取得公開的方法Method實例,並進行操作,就會引發IllegalAccessException例外,正如〈Java 9模組化概觀〉中談過的,只有讀取能力,並不代表著可使用被依賴模組中的類別,為了能使用反射操作公開的方法,被requires的模組必須exports套件,決定哪些套件具有存取能力(Accessibility)。

基本上,模組彼此合作時,多半會使用requires定義依賴的模組,而模組定義時,多半也會exports必要的套件,在這樣的情況下,公開成員的讀取能力與存取能力就像是預設行為,如果應用程式只是運用這種表層的反射機制,透過反射來操作公開成員也就沒有問題。

深層反射與服務提供

若只是requires與exports,而且,想要對非公開成員進行反射與操作的話,單純只是設定AccessibleObject的setAccessible(true),會引發InaccessibleObjectException,如果模組設計者確定允許此動作,須在module-info.java中,使用opens來定義哪些套件中的類別允許此動作,或者是使用open module,表示開放這個模組中全部的套件。

不過,java.base模組中的套件使用了exports,然而沒有設定opens的套件,也沒有直接open module java.base,因此不允許其他模組在反射時,對指定的非公開成員進行操作。設定上要注意的是,如果使用了open module,那麼,module中的定義就不能再有opens的獨立設定(因為已經開放整個模組了)。

過去許多API的設計,會定義出標準API,接著,應用程式依賴在標準API,而各廠商提供API實作。

範例之一就是JDBC,在Java 9支援模組化之後,為了讓標準API模組及實作模組之間,能夠鬆散耦合(loose coupling),在API模組上的module-info.java上,提供了uses,來設定這個模組會使用哪個介面提供服務,而在程式碼實作上,可以透過ServiceLoader運用反射,尋找模組路徑下,是否有PlayerProvider的具體實作,

在API實作的供應者模組部份,可以到module-info.java中,使用provides with,指定為哪個標準API來提供哪個實作類別,而模組不一定要exports必要的套件。在這樣的設計下,若要更換服務實作的提供者,只要換掉模組就可以了,若想看看實際範例,可在〈Module System Quick-Start Guide〉(https://goo.gl/sCS9wf)中找到。

未具名與自動模組

基於相容性,在採取模組設計之後,你可能會想到既有的程式庫或框架怎麼處理呢?

在Java 9之後,類別載入的來源可以是類別路徑與模組路徑。首先要知道的是,從類別路徑載入的類別,會被歸類為未具名模組(Unnamed module),而從模組路徑下載入的類別,都屬於某個具名模組(Named module)。

既有專案若暫且基於類別路徑方式運行,基本上是可以將既有JAR檔案放在類別路徑之中,成為未具名模組,這個模組沒有名稱,可以存取其他模組。然而,若決定漸進遷移至Java 9模組設計,可能就會遇到找不到類別路徑上類別的問題,因為自行明確定義的模組是顯式模組(Explicit module),而顯式模組不能直接存取未具名模組,當然,這時若嘗試使用反射存取未具名模組,也是行不通的,因而像是JDBC驅動程式的載入,可能就會失效。

此時,可以將既有的JAR檔案,改放在模組路徑之中。這會讓JAR成為自動模組(Automatic module),它是一種具名模組,名稱會自動由JAR檔案名稱按照一套規則來產生,並自動生成模組定義。我們可以使用jar的--describe-module來查看自動模組名稱,例如sqlite-jdbc-3.20.0.jar若置於模組路徑,會自動給予模組名稱sqlite.jdbc。如果無法從JAR檔案名稱中自動產生名稱的話,使用jar描述時會有錯誤,而被放到模組路徑的JAR檔案,也會導致執行java時產生錯誤訊息。

在Java 9中,自動模組可以存取其他模組,其他模組也可以存取自動模組,因而,自動模組就成了顯式模組與未具名模組之間溝通的橋樑,自動模組也會開放全部的套件(然而不是open module),因此,運用反射時,就可以深入地存取非公開成員。

來做個整理吧!在Java 9中,模組分為具名與未具名模組,而具名模組可分為顯式模組與自動模組,如果顯式模組使用open module定義,那會是開放模組(Open module),否則會是一般模組(Normal module)。因此,在Java 9中要能順利找到類別,就算是使用反射,就要搞清楚這些模組類型的差異。

類別載入器階層變動

除了搞清楚requires、exports、opens、uses與provides,以及模組類型的差異性之外,對於Java 9的類別載入器階層變動,也必須瞭解。

例如,在JDK9之前,從上而下,有Bootstrap、Extension及Application三個層次的類別載入器,在尋找類別時,都會先委託給上層載入器,一旦上層找不到時,才會試著由目前載入器自行載入。

在JDK9中,基本上維持三層,然而,Extension的角色被Platform載入器取代(因而不再支援擴充機制),只有java.base、java.logging、java.prefs與java.desktop模組的類別,是由Bootstrap載入,其他模組會是Platform來處理,因而Application可以直接委託給Platform,或者是Bootstrap,而Platform也可以直接委託給Application或Bootstrap。

由於導入了模組化設計,Java 9幾乎就是半個新平臺了,可以想見的,有些程式庫、框架或應用程式,未來並不會遷移到Java 9(這大概是為什麼Java 8會長期支援至2022年的原因之一了)。

這樣的變動,對Java來說,不能說是壞事,因為代表著Java社群關切的議題,已經整體地從語言轉到了模組設計這個層次,拼圖遇上反射只是個思考的機會與起點,關於模組化設計,開發者必須考量的事情,想必還有很多!

專欄作者

熱門新聞

Advertisement