提到模塊,大部分開發人員應該都不陌生。模塊化一直是軟件工程領域推薦的實踐。經過把一個項目劃分紅若干個相互依賴的模塊,能夠開發出所謂高內聚和低耦合的系統。 模塊化也有利於項目團隊的分工合做,以及代碼複用。java
模塊化的開發實踐,早在Java 9以前就已經出現,並且獲得了普遍的應用。好比,咱們一般會把一個項目劃分紅多個子項目。子項目之間經過依賴關係組織在一塊兒。目前已有的構建工具,包括Apache Maven和Gradle,都已經支持這樣的開發模式好久了。在Apache Maven中,咱們能夠在一個項目中建立多個不一樣的模塊;在Gradle中,咱們能夠在一個項目中建立不一樣的子項目。不管是Maven中的模塊仍是Gradle中的子項目,其實都是廣義上的模塊。全部這些模塊,在構建時,都會被打包成一個對應的JAR文件。因此這些模塊在運行時也是相對獨立的。sql
既然模塊化的實踐早就存在了,那爲何Java 9要花這麼大的力氣把模塊系統作到Java平臺中呢?實際上,添加模塊系統的Project Jigsaw以前是但願在Java 8中添加的,只是因爲影響太大難以按時完成,才被延遲到了Java 9中。Java 9也由於模塊系統被延遲了好幾回。數據庫
咱們能夠從幾個方面來談談模塊系統的必要性。服務器
從JDK和JRE的角度來講,Java應用在運行時須要JRE的支持。在Java 9以前,JRE的安裝是沒有辦法定製的。安裝的時候只能選擇安裝完整的JRE。JRE中包含的類庫和工具多種多樣,能夠知足全部應用的不一樣需求。然而對每一個具體的應用來講,JRE中包含的內容大部分都是多餘的。好比,服務器端的應用基本上不會用到桌面應用所需的Swing庫。隨着Java版本的不斷升級,其中所包含的內容只會愈來愈多,JRE所佔的空間也會愈來愈大。這無形之中增長了對存儲和帶寬的要求。Java 9中的模塊系統也包括把JDK模塊化。JDK 9一共由94個模塊組成。經過新增的jlink
工具能夠建立出每一個應用所獨有的Java運行時鏡像。在這樣的鏡像中,只須要包含應用真正依賴的JDK模塊便可。這能夠極大的減小應用所需的Java運行時環境的大小。對於簡單的應用來講,所需的運行時鏡像大小也就只有幾十兆。架構
從應用的角度來講,Java 9以前的應用在運行時都須要依賴CLASSPATH。把應用自己的JAR包和第三方庫的JAR包都放在CLASSPATH上,在運行時由JVM來查找並加載所需的Java類。對於一個複雜的應用來講,多個第三方庫的傳遞依賴之間可能互相沖突,產生所謂的JAR HELL問題。依賴關係是經過類型之間的引用關係來隱式的表達的。JAR HELL產生的根源在於CLASSPATH是單一的線性空間。框架
模塊系統的引入,增長了一個新的維度,也就是模塊。依賴關係經過模塊來顯式的聲明。這爲應用所使用的第三方庫之間的依賴關係,添加了更多的肯定性。ide
不少開發人員可能聽過說OSGi。做爲Java平臺上模塊化的一種實現方式,OSGi經過複雜的類加載器機制來實現不一樣模塊,以及同一模塊的不一樣版本之間的相互隔離。OSGi功能強大,可是其複雜性也是很高的。做爲Java平臺上的原生實現,Java平臺模塊系統更有吸引力。固然了,從功能上來講,模塊系統所能作的事情也相對有限。好比,模塊雖然能夠記錄版本號,可是版本號在模塊解析時是被忽略的。模塊系統爲開發模塊化應用提供了一個Java平臺的原生解決方案。對於大部分應用來講,模塊系統所提供的功能應該足夠了。模塊化
那麼, 到底什麼是模塊呢?根據Oracle的Java平臺集團的首席架構師Mark Reinhold的論述:工具
模塊是一個命名的、自我描述的代碼和數據的集合。模塊的代碼被組織成多個包,每一個包中包含Java類和接口;模塊的數據則包括資源文件和其餘靜態信息。ui
從上述的定義能夠看出來,Java 9中的模塊與如今咱們常用的Maven中的模塊或Gradle中的子項目並無太大的區別。它們能夠很容易的轉換成Java 9模塊。
每一個模塊都須要有本身的名字,稱爲命名模塊。名稱是模塊的惟一標識符,也是進行解析時的惟一查找條件。推薦的作法是採用與Java包相同的命名規則,也就是倒轉域名的格式,如com.mycompany.mymodule
。
模塊與通常子項目的區別在於,模塊的源代碼的根目錄下包含module-info.java
文件來做爲模塊的描述符。該文件會被編譯成module-info.class
文件出如今打包好的模塊工件中,通常是模塊的JAR文件中。module-info.class
文件的出現與否,就是Java 9中模塊的基本特徵。
在module-info.java
文件中,咱們能夠用新的關鍵詞module
來聲明一個模塊,以下所示。下面給出了一個模塊com.mycompany.mymodule
的最基本的模塊聲明。
module com.mycompany.mymodule { }
下面咱們介紹模塊聲明文件中的重要組成部分。
模塊以前存在着依賴關係。每一個模塊能夠經過requires
來聲明其對其餘模塊的依賴關係。依賴一個模塊並不意味着就自動得到了訪問該模塊中包含的Java類型的許可。一個模塊能夠聲明其中包含的哪些包(package)是可供其餘模塊訪問的。只有被導出的包才能被其餘模塊所訪問。而在默認的狀況下,是沒有任何包被導出的。咱們經過在模塊聲明文件中的exports
來導出包。導出的包中包含的public
和protected
類型,以及這些類型中包含的public
和protected
成員是能夠被依賴它們所在模塊的其餘模塊來訪問的。
須要注意的是,當導出一個包時,只有該包中的類型會被導出,子包中的類型不會被導出。若是聲明導出的包爲com.mycompany.mymodule
,則相似com.mycompany.mymodule.A
和com.mycompany.mymodule.B
這樣的類型會被導出;而相似com.mycompany.mymodule.impl.C
或com.mycompany.mymodule.test.demo.D
這樣的類型則不會。若是須要導出子包,必須使用exports
來對每一個子包進行顯式聲明。
若是一個模塊中的類型不能被其餘模塊所訪問,那麼該類型等同於該模塊中的私有類型或類型中的私有成員。試圖在源代碼中使用這些類型或成員會產生編譯錯誤。在運行時,則會由JVM拋出java.lang.IllegalAccessError
錯誤;若是試圖經過Java的反射API來訪問,則會拋出java.lang.IllegalAccessException
異常。
下面是一個使用了requires
和exports
的模塊聲明文件。模塊com.mycompany.moduleA
導出了包com.mycompany.moduleA
,同時依賴模塊com.mycompany.moduleB
。
module com.mycompany.moduleA { exports com.mycompany.moduleA; requires com.mycompany.moduleB; }
在導出一個包時,默認狀況下是對全部聲明依賴了該包所在模塊的所有其餘模塊可見。在某些狀況下,咱們會但願限制某些包對於其餘模塊的可見性。舉例來講,一個包可能在最先的設計中是對全部模塊都公開的,可是該包在後來的版本更新中被新的包所替代,所以被聲明爲廢棄的(deprecated)。這個被廢棄的包應該只能被遺留代碼所使用。在新的版本中,包含該包的模塊應該只是把該包導出給還在使用遺留代碼的模塊。這樣能夠確保遺留代碼不會被錯誤的繼續使用。經過在exports
聲明後添加to
語句,能夠指定容許訪問該包的模塊名稱。
好比在下面的JDK模塊java.rmi
的模塊聲明中能夠看到,包sun.rmi.registry
只對jdk.management.agent
導出。
module java.rmi { requires java.logging; exports java.rmi.activation; exports com.sun.rmi.rmid to java.base; exports sun.rmi.server to jdk.management.agent, jdk.jconsole, java.management.rmi; exports javax.rmi.ssl; exports java.rmi.dgc; exports sun.rmi.transport to jdk.management.agent, jdk.jconsole, java.management.rmi; exports java.rmi.server; exports sun.rmi.registry to jdk.management.agent; exports java.rmi.registry; exports java.rmi; uses java.rmi.server.RMIClassLoaderSpi; }
當模塊A依賴模塊B時,模塊A能夠訪問模塊B中導出的public
和protected
類型。咱們把這種關係稱爲模塊A 讀取(read)模塊B。同理,若是模塊B讀取模塊C,模塊B也能夠訪問模塊C導出的public
和protected
類型。也就是說,模塊B能夠在其包含的代碼中,使用模塊C中的類型來做爲方法的參數或是返回類型。
模塊C的聲明以下:
module C { exports ctest; }
模塊B的聲明以下:
module B { requires C; exports btest; }
模塊A的聲明以下:
module A { requires B; }
假設模塊A、B和C中分別定義了類MyA
、MyB
和MyC
。其中類MyA
的定義以下所示。其中MyB
來自模塊B,其中的方法getC()
返回的是模塊C中的類MyC
。
package atest; import btest.MyB; public class MyA { public static void main(String[] args) { new MyB().getC().sayHi(); } }
若是模塊A的聲明如上述所示,會發現MyA
沒法經過編譯。這是由於模塊A在其module-info.java
文件中沒有聲明對模塊C的依賴關係,所以模塊A並無讀取模塊C。模塊的讀取關係默認並非傳遞的。
爲了解決這個問題,能夠在requires
中添加新的描述符transitive
來聲明一個依賴關係是傳遞的。一個模塊中聲明爲可傳遞的依賴模塊,能夠被依賴該模塊的其餘模塊來讀取。這種讀取關係稱爲隱式可讀性(implicit readability)。對於上面的例子來講,只須要把模塊B對模塊C的依關係聲明爲可傳遞便可。這樣模塊B的可傳遞依賴模塊C,就能夠被依賴模塊B的模塊A所讀取,從而模塊A的代碼能夠被成功編譯。
module B { requires transitive C; exports btest; }
靜態依賴是一種特殊的依賴關係,經過requires static
來進行聲明。靜態依賴所聲明的模塊在編譯時是必須的,可是在運行時是可選的。
module demo { requires static A; }
靜態依賴對於框架和第三方庫來講比較實用。假設咱們須要開發一個能夠和不一樣數據庫交互的庫。這個庫所在的模塊可使用靜態依賴來聲明對所支持的數據庫JDBC驅動的依賴關係。在編譯時,庫中的代碼能夠訪問這些驅動中的類型;在運行時,用戶只須要添加所須要使用的驅動便可。若是不使用靜態依賴,用戶必需要添加全部支持的驅動才能完成模塊的解析。模塊系統在解析時,若是遇到沒法找到的模塊,會報錯並退出。而要求用戶添加全部支持的驅動模塊也是不現實的。這就是靜態依賴實用的地方。
Java平臺有本身的服務接口和服務提供者機制。經過類java.util.ServiceLoader
來完成服務提供者的查找。服務機制主要用在JDK自己以及第三方框架和庫中。服務機制的一個典型應用是JDBC驅動。每一個JDBC驅動都須要提供服務接口java.sql.Driver
的實現。
Java 9以前,ServiceLoader
經過掃描CLASSPATH來查找特定服務接口的實現類。在Java 9中, 模塊成了代碼的組織單元。模塊聲明文件中提供了與服務使用者和提供者相關的聲明。
假設存在一個服務接口com.mycompany.mymodule.Demo
,做爲服務的提供者,能夠用以下的方式來聲明。其含義是該模塊提供了服務接口com.mycompany.mymodule.Demo
的實現類com.mycompany.mymodule.d.DemoImpl
。
module com.mycompany.mymodule.D { provides com.mycompany.mymodule.Demo with com.mycompany.mymodule.d.DemoImpl; }
當模塊須要使用一個服務接口時,能夠添加以下所示的聲明。
module com.mycompany.mymodule.E { uses com.mycompany.mymodule.Demo; }
接着就可使用ServiceLoader
來查找服務接口的提供者了。
ServiceLoader.load(Demo.class)
在模塊聲明文件中,能夠在module
以前添加open
描述符來把該模塊聲明爲開放的。一個開放的模塊在編譯時只容許其餘模塊訪問其經過exports
聲明來顯式導出的包。而在運行時,模塊中的全部包都是被導出的,包括那些沒有經過exports
聲明的包。一樣的,也能夠經過Java反射API來訪問全部包中的全部Java類型。全部Java類型中包括私有類及類型中的私有成員。若是使用Java反射API來繞開Java語言的訪問檢查機制,如AccessibleObject
類的setAccessible()
方法,就能夠訪問開放模塊中的私有類型和成員。
open module E { exports etest; }
對於每一個具體的包,也可使用opens
來把它聲明爲開放的。開放的包能夠經過Java反射API來訪問。就如同開放模塊同樣,使用反射API能夠訪問開放包中的全部類型及其全部成員。開放包的聲明也支持經過to
語句來指定可訪問的模塊名稱。
module F { opens ftest1; opens ftest2 to G; }
開放模塊和包的目的主要是爲了解決已有代碼的兼容性問題,尤爲是在使用Java反射API時。在升級已有代碼到Java 9模塊系統時,若是遇到了與反射API相關的問題,能夠把須要被反射API訪問的模塊或包聲明爲開放的。
若是模塊系統須要加載一個來自不在任何模塊所聲明導出的包中的Java類型時,它會嘗試從CLASSPATH中加載。若是該Java類型被成功加載,那麼該類型被視爲是一個特殊模塊的成員。該特殊的模塊稱爲未命名模塊(unnamed module)。未命名模塊的特殊性在於它讀取全部其餘的模塊,而且導出全部內部包含的包。
若是一個類型是從CLASSPATH中加載的,那麼做爲未命名模塊中的成員,它能夠訪問全部其餘模塊中所導出的包,也包括Java平臺內部的模塊。正由於這樣,Java 9以前編寫的應用,能夠不通過任何改動就運行在Java 9之上。雖然未命名模塊導出了全部內部的包,可是其它命名模塊中的代碼並不能訪問未命名模塊中的類型。咱們也沒辦法經過requires
來聲明對未命名模塊的依賴關係。這樣的限制是必須的,不然咱們就失去了引入模塊系統的全部好處,又從新回到了依靠CLASSPATH的老路上去了。未命名模塊的主要目的是保持向後兼容性。若是一個包同時在某個命名模塊和未命名模塊中出現,那麼在未命名模塊中的包會被忽略。因此在CLASSPATH中的包不會干擾在命名模塊中的代碼。
每一個類加載器都有本身的未命名模塊。由該類加載器加載的類型,若是來自CLASSPATH,那麼會做爲其未命名模塊的成員。能夠經過ClassLoader
類的getUnnamedModule()
來獲取到其對應的未命名模塊。
因爲Java 9是向後兼容的,對於已有的應用來講,不必定要升級到使用模塊。不過仍是建議升級來利用模塊系統的好處。
推薦的升級方式是採用自底向下的作法,也就是說從依賴關係樹中的葉子節點開始作起。舉例來講,若是一個應用有3個子模塊或子項目,A、B和C。它們之間的依賴關係是A -> B -> C。當升級該應用到Java 9時,推薦的作法是從C開始,而後再依次是B和A。這樣的話,當C被升級爲模塊以後,A和B都還在未命名模塊中,能夠繼續訪問模塊C中的類型。這是由於咱們以前提到的,未命名模塊能夠讀取全部的其它命名模塊。而後咱們升級B爲一個命名模塊,並聲明其依賴模塊C。最後再把A升級爲模塊,就完成了該項目的升級。
然而這種自底向上的升級方法並不老是可行的。有些庫多是第三方所維護的,咱們沒有辦法控制這些庫升級到模塊的時間。咱們仍然但願能夠升級那些依賴這些第三方庫的代碼。咱們沒有辦法直接把應用自己的代碼升級爲模塊,而把第三方庫放在CLASSPATH中。這樣的話,這些第三方庫會出如今未命名模塊中。而咱們以前已經說過,命名模塊是不能訪問未命名模塊中的Java類型的。
爲了解決這個問題,Java模塊系統中有另一個自動模塊(automatic module)的概念。咱們只須要把這些第三方庫放在模塊路徑中,它們會被轉換成自動模塊。 與其餘顯式建立的命名模塊不一樣的是,自動模塊是從普通的JAR文件中自動建立出來的。這些JAR文件中並無包含模塊聲明文件module-info.class
。自動模塊的名稱來自於JAR文件的清單文件MANIFEST.MF
中的屬性Automatic-Module-Name
,或從JAR文件的名稱中自動推斷出來的。其餘模塊可使用該名稱來聲明對該自動模塊的依賴。推薦的作法是使用清單文件的屬性Automatic-Module-Name
,比依賴JAR文件名稱的作法要更加可靠。
自動模塊的特殊性體如今下面幾個方面:
自動模塊是CLASSPATH和命名模塊之間的橋樑。最終的目的固然是把Java 9以前的那些子模塊、子項目和庫都升級到Java 9的命名模塊。可是在升級過程當中,咱們能夠把這些子模塊、子項目和庫的JAR文件加入到模塊路徑中做爲自動模塊來使用。
在下面的代碼中,模塊D中的類dtest.MyD
使用了Google Guava庫中的com.google.common.collect.Lists
。
package dtest; import com.google.common.collect.Lists; public class MyD { public static void main(String[] args) { System.out.println(Lists.newArrayList("Hello", "World")); } }
Gauva尚未升級爲Java 9的模塊。在模塊D的描述文件中,咱們可使用requires guava
來聲明對Guava的依賴關係。guava
是Guava庫對應的自動模塊的名稱,從JAR文件名推斷而來。
module D { requires guava; }
下面咱們簡單介紹一下Java應用運行時的模塊解析過程,以及模塊相關的Java API。在啓動JVM運行應用時,模塊解析的過程從應用的主模塊開始,並根據模塊依賴關係來遞歸地解析其餘模塊。模塊解析的結果是一個由Configuration
類表示的可讀性圖。可讀性圖是一個有向圖,其中頂點表示的是已解析模塊(ResolvedModule
),而邊表示的是模塊的讀取關係。接下來的任務是從可讀性圖中建立出模塊層(ModuleLayer
)。建立的方式是爲圖中的每個已解析的模塊指定一個加載其中類型的類加載器。JVM中至少包含一個非空的模塊層,即啓動模塊層(boot layer),在JVM啓動時自動建立。大部分的應用只使用啓動模塊層。在一些複雜的場景中,能夠建立多個模塊層來知足不一樣的需求。一個模塊層能夠有多個父模塊層。模塊層中的模塊能夠讀取其父模塊層中的其餘模塊。模塊層中包含的是運行時模塊,由Module
來表示。對於Configuration
中的每一個ResolvedModule
都會有一個對應的Module
對象。
因爲類型都在模塊中,經過Class
類的新方法getModule()
能夠獲取其所在模塊的Module
對象。
咱們只是簡單的討論Java 9模塊系統中的一些基本概念。這些概念是開發新的模塊化應用的基礎。模塊系統所包含的內容還有不少沒有討論到。好比對已有的JDK工具,如javac
、java
和jar
的改動,以及新增的工具jlink
。這部份內容能夠參考Java 9的官方文檔。