子產品系統的說明
該文章是關于JSR 376: The Java Platform Module System中提議的Jigsaw項目原型的非正式版概覽。另一篇文章描述了JDK相關工具以及API的一些增強特性,不過這不在JSR的範圍之内了。
就像JSR376中描述的一樣,子產品化系統的目标是提供:
- 可靠的配置,用程式元件的方式來替代脆弱的、易出錯的classpath機制,并且可以顯示的聲明對其他元件的依賴。
- 強封裝,允許元件聲明哪些類型可以對其它元件開放,哪些不可以。
這些特性将會為應用開發者,庫開發者,以及Java SE平台的實作人員帶來直接跟間接的好處,因為它們将使得系統具有更好的拓展性,更高的完整性,以及更高的性能。
目錄
1. 定義子產品
子產品聲明.子產品工件.子產品描述符.平台子產品
2. 使用子產品
子產品路徑.解析.可讀性.可通路性.隐式可讀性
3. 相容性&遷移
未命名的子產品.由下而上的遷移.自動子產品.類路徑的橋接
4. 服務
5. 進階話題
映射.映射可讀性.類加載器.未命名的子產品.層.有限制的導出
總結
緻謝
這是該文檔的第二版,相比最初版,該版本介紹了相容性與遷移,修改了映射可讀性,重新編排文字,已改善叙述流程,并且分了兩級以便于導航閱讀。
目前的設計中依舊有很多問題,他們的解決方案将會在該文檔的未來版本中給予說明。
1 定義子產品
為了能夠在即對開發者友好,又能支援目前工具鍊的前提下,提供可靠的配置以及強封裝性,我們把子產品看作一種全新的Java基礎程式元件。一個子產品就是一個由代碼跟資料組成的有名稱的且自描述的集合。代碼被組織為一個包含類型,如Java類跟接口的包的集合,而資料則包括資源或其他形式的靜态資訊。
1.1 子產品聲明
一個子產品的自描述性很好的展現在了它的
子產品聲明
上,一種Java程式設計語言中定義的新結構。一個可能的最簡單的子產品定義差不多隻需要指定子產品的名字即可:
module com.foo.bar { }
可以定義一個或多個
requires
語句來指定該子產品在編譯期以及運作時所依賴的其他子產品的名字:
module com.foo.bar {
requires org.baz.qux;
}
最後,
exports
語句用來聲明指定的包中的所有的,并且隻有,
public
類型對其他子產品可見:
module com.foo.bar {
requires org.baz.qux;
exports com.foo.bar.alpha;
exports com.foo.bar.beta;
}
如果一個子產品的聲明中,不包含
exports
語句,則表明該子產品不會對其他子產品公開任何類型。
按照約定,源碼的子產品定義被放在一個名為
module-info.java
的檔案中,該檔案位于源碼層次結構的根目錄下。com.foo.bar子產品的源檔案如下:
module-info.java
com/foo/bar/alpha/AlphaFactory.java
com/foo/bar/alpha/Alpha.java
...
按照約定,子產品定義被編譯到
module-info.class
檔案中,同樣會被放到類檔案的輸出目錄中去。
子產品名稱,就像包名稱,必須不能有沖突。就像我們一直推薦的包取名的模式一樣,我們推薦使用翻轉域名的模式來給子產品取名字。是以,通常子產品的名字是導出包的名字的字首,不過這種關系并不是強制性的。
一個子產品的定義不包含版本字元串,也不包含它所依賴的其他子產品的版本号,這是有意為之的:解決版本選擇問題并不是子產品系統的一個目标,我們更傾向于将它留給建構工具或者容器來解決。
由于一些原因,子產品聲明是Java程式設計語言的一部分,而不是一門自成體系的語言或符号,其中最重要的就是子產品資訊在編譯期和運作時必須都可用,以達到跨階段的高保真度,例如,確定子產品系統在編譯期跟運作時能以相同的方式工作,相應的,這就可以避免一些錯誤,或者至少在編譯期能更早的報告錯誤,然後診斷并修複。
将源檔案中的子產品聲明與子產品中的其他源檔案一起轉換為Java虛拟機所需的類檔案是建立保真度的一種正常手段。這種方式對于開發人員相當熟悉,并且對于IDE或者建構工具的支援也不困難。尤其是IDE,可以根據元件的項目描述中已經提供的資訊來合成requires語句,進而為現有元件提供初始子產品聲明的建議。
1.2 子產品工件
現有工具可以建立,操作和使用JAR檔案,是以為了友善采用和遷移,我們定義了子產品化的JAR檔案。一個子產品化的JAR檔案就像普通的JAR檔案一樣,除了在根目錄下還包含一個
module-info.class
檔案。例如,上述
com.foo.bar
子產品的jar檔案中的内容可能像下面這樣:
META-INF/
META-INF/MANIFEST.MF
module-info.class
com/foo/bar/alpha/AlphaFactory.class
com/foo/bar/alpha/Alpha.class
...
子產品化JAR檔案可以作為一個子產品,在這種情況下,它的
module-info.class
檔案被用來包含子產品的聲明。或者,可以把它放到普通的類路徑中,這種情況下,它的
module-info.class
檔案将被忽略。子產品化JAR檔案允許庫的維護者既可以以工件的形式傳輸,來作為Java SE9 或者更高版本中的子產品來使用,也可以作為适用于所有版本中類路徑下的正常JAR檔案。我們期望Java SE9的實作中包含一個增強的jar工具,可以很容易的建立子產品化的JAR檔案。
為了子產品化Java SE平台的參考實作JDK,我們将引入一種新的工件格式,以适應本機代碼,配置檔案以及其他各種天生并不适配JAR檔案的資料。這種格式利用了在源檔案中定義子產品并編譯到類檔案中的另一個優勢,即類檔案跟其他任何特定的工件格式無關。這種新格式,暫時被叫做“JMOD”,它的标準名稱至今是個懸而未決的問題。
1.3 子產品描述符
将子產品聲明編譯到類檔案中還有一個最終的優勢,就是類檔案已經具備一種精确定義且可拓展的格式。是以在一個更廣泛的意義上,我們可以将
module-info.class
作為子產品描述符,其中包括源碼級别的子產品聲明的編譯格式,以及聲明被初始編譯後插入到類檔案屬性中的各種其他的資訊。
例如,IDE或者建構期的打包工具可以将包含像子產品版本,标題,描述以及許可證等文檔資訊插入到屬性中。可以通過子產品系統的反射工具在編譯期以及運作時來讀取這些資訊用來生成文檔,診斷以及調試等。下遊工具也可以使用它來建構特定于作業系統的程式包工件。将會有一系列特定的屬性被标準化,但是,既然Java的類檔案格式是可拓展的,那麼其他的工具或者架構在必要的時候同樣可以定義自己的屬性。非标準的屬性對子產品系統本身不會造成影響。
1.4 平台子產品
Java SE 9 平台規範将使用子產品系統把平台劃分為一系列子產品。Java SE 9平台的實作可能包含所有的平台子產品,也可能隻包含其中的一部分。
任何情況下,子產品系統中唯一明确知道的子產品是被叫做
java.base
的基子產品。基子產品定義并導出了所有的平台核心包,同樣包括它自身:
module java.base {
exports java.io;
exports java.lang;
exports java.lang.annotation;
exports java.lang.invoke;
exports java.lang.module;
exports java.lang.ref;
exports java.lang.reflect;
exports java.math;
exports java.net;
...
}
基子產品将會一直存在,任何其他子產品都将隐式的依賴于基子產品,然而基子產品不會依賴于其他任何子產品。
其他的平台子產品将使用
java.
字首,并且可能包含像用來連接配接資料庫的
java.sql
子產品,用來處理XML的
java.xml
子產品,以及日志處理的
java.logging
子產品。Java SE 9平台規範中沒有定義的子產品,而在JDK中定義的子產品,習慣上将會使用
jdk.
字首。
2 使用子產品
個人子產品可以在子產品工件中定義,也可以内置于編譯期或者運作時環境中。為了在任意階段都可以使用他們,子產品系統需要定位它們,然後确定互相依賴關系,以便提供可靠的配置以及強封裝性。
2.1 子產品路徑
為了定位定義在工件中的子產品,子產品系統需要搜尋主機中定義的
子產品路徑
。子產品路徑是一個序列,其中每個元素要麼是子產品工件,要麼是包含子產品工件的目錄。子產品系統會按順序搜尋子產品路徑中的元素去尋找第一個滿足條件的子產品。
子產品路徑跟類路徑有着本質上的差別,并且更加健壯強大。類路徑天生的脆性是由于它隻是定位所有工件中單個類型的手段,而不能區分工件本身的不同。這對于提前判斷工件是否缺失尤其重要。它也允許不同的工件在相同的包中定義類型,即便這些工件是同一個程式元件的不同版本甚至是完全不同的元件。
相反,子產品路徑是用來定位整個子產品而不是單個類型的手段。如果子產品系統無法在子產品路徑中找到某個特定的工件依賴,或者在同一個目錄下遇到兩個相同名稱的工件,那麼編譯器或者虛拟機将會報告該錯誤并且退出。
編譯期或者運作時内置的子產品,以及子產品路徑中定義的工件統稱為
可觀察到的子產品
。
2.2 解析
假設我們有一個應用程式使用上面的com.foo.bar子產品和平台的java.sql子產品。包含應用程式核心的子產品聲明如下:
module com.foo.app {
requires com.foo.bar;
requires java.sql;
}
對于該初始應用子產品,子產品系統通過定位其它的可觀察到的子產品來解析
requires
語句中的依賴,然後再解析這些可觀察到的子產品的依賴,直到所有子產品的所有依賴都得到解析。這種傳遞閉包計算的結果是形成一個
子產品圖
,對于每一個依賴其他子產品的子產品都包含一個從第一個子產品到第二個子產品的有向邊。
為了建構com.foo.app子產品的子產品圖,子產品系統會檢查java.sql子產品的聲明:
module java.sql {
requires java.logging;
requires java.xml;
exports java.sql;
exports javax.sql;
exports javax.transaction.xa;
}
同樣會檢查com.foo.bar子產品的聲明,以及org.baz.qux,java.logging,跟 java.xml子產品。簡單起見,最後三個并未包含提到,是因為他們并不包含對其他子產品的依賴。
基于所有這些子產品聲明,com.foo.app子產品的子產品圖應該包含以下節點跟有向邊:
該圖中深藍色的線表示
requires
語句中表明的顯示的依賴關系,而淺藍色的線表示每個子產品對基子產品的隐式的依賴。
2.3 可讀性</a>
當子產品圖中的一個子產品直接依賴其他子產品時,那麼第一個子產品中的代碼就可以引用第二個子產品中的類型。是以我們說第一個子產品可
讀取
第二個子產品,或者等價的說,第二個子產品對第一個子產品是
可讀的
。是以,在上面的圖中,com.foo.app子產品可以讀取com.foo.bar以及java.sql子產品,但是無法讀取org.baz.qux,java.xml,以及java.logging子產品。java.logging子產品對于java.sql子產品是可讀的,但對其他子產品不可讀。(根據定義,任何子產品對自己是可讀的)
子產品圖中定義的可讀性關系是可靠配置的基礎:子產品系統確定對每一個其他子產品依賴的精确比對,保證子產品圖是無回路的,每一個子產品最多讀取一個給定包的子產品,以及定義相同包名的子產品互不影響。
可靠的配置不僅更可靠,而且更快。當一個子產品中的代碼引用某個包中的類型時,我們可以确定該包被定義在了該子產品或者該子產品可讀的其他子產品中。是以當搜尋某個具體的類型時就沒有必要搜尋多個子產品,更不用糟糕到的去搜尋整個類路徑。
2.4 可通路性
子產品圖中定義的可讀性關系以及子產品聲明中的
exports
語句是強封裝性的基礎。Java編譯器跟虛拟機認為一個子產品的某個包中的公共類型對其他子產品中的代碼
可通路
的條件是第一個子產品對第二個子產品是可讀的,并且第一個子產品導出了那個包。比如,兩個類型S跟T定義在兩個不同的子產品中,并且T是公共的,那麼S可以通路T的條件是:
- S所在子產品可讀去T所在子產品,并且
- T所在子產品導出了T所在的包。
就像私有方法跟私有屬性不可被其它類通路一樣,一個類型是無法透過不可通路的子產品邊界被引用的。任何嘗試對它通路都會得到一個編譯器報告的錯誤,或者虛拟機抛出的
IllegalAccessError
,或者反射運作時API抛出的
IllegalAccessException
。是以即使當一個類型被定義為公共的,但是假如它所在的包沒有在子產品聲明中被導出,那麼它也隻能被本子產品内的代碼通路。
如果透過子產品邊界,一個方法或者屬性的外圍類是可以通路的,并且該成員本身的聲明也是允許通路的,那麼它也可以透過子產品邊界被通路。
來看一下上面的子產品圖中的強封裝性是如何工作的,我們給每個子產品貼上它所導出的包的标簽:
子產品com.foo.app子產品中的代碼可以通路com.foo.bar.alpah包中的公共類型,因為com.foo.app依賴于它,是以可讀取com.foo.bar子產品,并且com.foo.bar子產品導出了com.foo.bar.alpah包。 如com.foo.bar包含一個内部包com.foo.bar.internal,那麼com.foo.app中的代碼不能通路該包中的任何類型,因為com.foo.bar沒有導出它。com.foo.app中的代碼不能通路org.baz.qux包中的類型,因為coom.foo.app不依賴它,是以不可讀去該子產品。
2.5 隐式可讀性
如果一個子產品可以讀取另一個子產品,某些情況,邏輯上也應該可以讀取其他子產品。
例如,平台的java.sql子產品依賴java.logging跟java.xml子產品,不僅因為它的代碼實作中使用了這些子產品中的類型,而且還因為它定義的類型的方法簽名引用了這些子產品中的類型。 java.sql.Driver接口中聲明了下面這個公共方法:
public Logger getParentLogger();
其中Logger是java.logging子產品中導出的java.util.logging包中聲明的類型。
假設,com.foo.app子產品中的代碼為了擷取logger并且記錄日志而調取了這個方法:
String url = ...;
Properties props = ...;
Driver d = DriverManager.getDriver(url);
Connection c = d.connect(url, props);
d.getParentLogger().info("Connection acquired");
如果com.foo.app子產品的聲明像上面提到的那樣,那麼這段代碼将不能工作:getParentLogger方法傳回一個Logger,它是一個在java.logging子產品中聲明的類型,它對com.foo.app子產品是不可讀的,是以上面對Logger類中的info方法的調用在編譯期跟運作時都将失敗,因為該類不可通路,是以該方法同樣不可通路。
該問題的一個解決方案是所有依賴java.sql子產品并且包含使用getParentLogger方法傳回的Logger對象的代碼的子產品作者記得聲明一個對java.logging子產品的依賴。當然這種方式是不可靠的,因為它打破了最少意外的原則:如果一個子產品依賴第二個子產品,那麼很自然的希望每個需要使用第一個子產品的類型,即使是在第二個子產品中定義的類型,都将對于僅僅依賴第一個子產品的子產品是直接可通路的。
是以我們拓展子產品的聲明以便一個子產品可以将它所依賴的其他子產品的可讀性授予依賴它的任何子產品。這種
隐式的可讀性
通過在
requires
語句中包含一個public修飾符來表達。java.sql子產品的聲明實際上是這樣:
module java.sql {
requires public java.logging;
requires public java.xml;
exports java.sql;
exports javax.sql;
exports javax.transaction.xa;
}
public修飾符意味着任何依賴java.sql的子產品不僅可讀取java.sql子產品,而且也可以讀取java.logging跟java.xml子產品。是以com.foo.app子產品的子產品圖将包含兩條用綠色邊連接配接到java.sql子產品的深藍色的邊,因為它們是因該子產品而隐式可讀的:
現在com.foo.app子產品可以包含通路java.logging及java.xml子產品導出包中所有公共類型的代碼了,即使它的聲明中沒有提到這兩個子產品。
通常,如果一個子產品導出一個包,該包包含一個簽名引用第二個子產品的類型,那麼第一個子產品的聲明應該包含一個對第二個子產品的
requires public
依賴。這樣可以確定其它依賴第一個子產品的子產品自動的對第二個子產品具有可讀性,以及可以通路該子產品導出包中的所有公共類型。
3 相容性&遷移
到目前為止,我們已經看到了如何從頭開始定義子產品,将它們打包成子產品工件,并且把他們與其他平台内置的子產品或者定義在其他工件中的子產品一起使用。
當然,大部分的Java代碼是在子產品系統引入前就寫好的,并且還必須像現在這樣不需任何改變依舊能正常運作。是以,即使平台本身是由子產品組成,子產品系統也仍然可以編譯運作由類路徑中的Jar檔案組成的應用。并且也可以将先用的應用以一種靈活漸進的方式遷移到子產品化中來。
3.1 未命名子產品
如果有個需求是在任意已知的子產品中加載一個沒有定義包的類型,那麼子產品系統會嘗試從類路徑中加載它。如果加載成功,那麼會被認為是一個特殊的被稱為
未命名子產品
的成員,以便確定每個類型關聯到某個子產品上。未命名子產品就像是進階層面上的現有的未命名包的概念。當然,以後我們就把那些有名稱的子產品稱作
命名的子產品
。
未命名的子產品可以讀取其他任意子產品。是以從類路徑中加載的任意類型中的代碼都将可以通路任意其他可讀子產品的導出類型,這些可讀子產品預設包括命名子產品,内置的平台子產品。是以,在Java SE 8上編譯和運作的現有類路徑應用程式将在Java SE 9上以完全相同的方式進行編譯和運作,隻要它隻使用了标準的,不被廢棄的Java SE API即可。
未命名子產品會導出它的所有包。就像我們将在下面看到的,這會使得遷移更加靈活。然而,這不意味着命名子產品中的代碼可以通路未命名子產品中的類型。事實上,命名子產品甚至不能聲明對未命名子產品的依賴。這個限制是有意為之的。因為允許命名子產品依賴類路徑中的任意内容是不可能做到可靠的配置的。
如果一個包被定義在了命名子產品跟未命名子產品中,那麼未命名子產品中的包會被忽略。即使面對類路徑的混亂這依舊保持可靠的配置,即確定每一個子產品依舊最多隻會讀取一個定義特定包的子產品。如果在我們上面的例子中,一個類路徑下的JAR檔案包含com/foo/bar/alpha/AlphaFactory.class類,那麼該檔案将永遠不會被加載,因為com.foo.bar.alpha包是com.foo.bar子產品的導出包。
3.2 由下而上的遷移
把從類路徑中加載的類型當作未命名子產品的成員允許我們以增量,自下而上的方式将現有應用程式的元件從JAR檔案遷移到子產品。
假設,上面提到的應用最初是在Java SE 8下建構的,作為放在類路徑下的一組類似命名的JAR檔案。如果我們按原樣在Java SE 9中運作它們,那麼這些JAR檔案中的所有類型都會被定義到未命名子產品中。該子產品可以讀取所有其他子產品,包括所有内置的平台子產品,簡單起見,假設隻讨論前面提到的java.sql,java.xml,java.logging,以及java.base子產品,因為我們得到如下子產品圖:
我們可以直接将org-bar-qux.jar轉化為命名子產品,因為我們知道它不會引用其它兩個JAR檔案中的任何類型,是以,作為一個命名子產品,它不會引用任何被留在未命名子產品中的類型。(我們碰巧從最初的例子中知道這點,但是如果我們還不知道,那麼我們可以使用jdeps)我們寫一個org.baz.qux的子產品聲明,并把它添加到子產品的源碼中,然後編譯它,并将結果打包成一個子產品JAR檔案。如果我們把這個JAR檔案放到子產品路徑中,并且把其它的留在類路徑中,我們會得到如下增強的子產品圖:
com-foo-bar.jar和com-foo-app.jar中的代碼可以繼續工作,因為未命名子產品可以讀取任意命名子產品,包括新的org.baz.qux子產品。
我們可以用相似的方式處理com-foo-bar.jar以及com-foo-app.jar,最終重新繪制前面顯示的子產品圖:
我們知道對原始的JAR檔案中的類做了什麼,當然可以一步就将三個應用子產品化。然而,如果org-baz-qux.jar是由一個完全不同的團隊或者組織單獨維護的,那麼它可以在其他兩個元件之前被子產品化,同樣com-foo-bar.jar可以在com-foo-app.jar之前被子產品化。
3.3 自動子產品
自下而上的遷移是直截了當的,當并不可能總是如此。即使org-baz-qux.jar的維護者還沒有将它轉化為合适的子產品-或者永遠不會将它子產品化-我們依舊想子產品化我們自己的com-foo-app.jar和com-foo-bar.jar元件。
我們已經知道com-foo-bar.jar中的代碼引用org-baz-qux.jar中的類型。如果我們把com-foo-bar.jar轉換為命名子產品com.foo.bar,但是把org-baz-qux.jar留在類路徑中,那麼會導緻代碼不可用:org-baz-qux.jar中的類型會被定義到未命名子產品中,而com.boo.bar是命名子產品,是無法依賴于未命名子產品的。
是以我們必須以某種方式讓org-baz-qux.jar以命名子產品的方式運作,以便com.foo.bar可以依賴它。我們可以fork一個org.baz.qux源碼的分支然後我們自己将其子產品化,但是維護者不願将它合并到上遊倉庫中,那麼就不得不一直維護這個分支。
相反,我們可以将org-baz-qux.jar作為一個
自動子產品
,原封不動的将其放到子產品路徑中而不是類路徑下。這樣将會定義一個可觀察的子產品,它的名字将由JAR檔案衍生而來
org.baz.qux
,以便非自動子產品可以用正常的方式依賴它:
自動子產品是一個隐式定義的命名子產品,因為它沒有子產品聲明。相比之下,一個普通的命名子產品會有子產品聲明來顯示的定義;我們以後把這類子產品看作顯示子產品。
沒有好辦法可以提前告知自動子產品可能依賴哪些其他子產品。是以在一個子產品圖被确定之後,自動子產品可以讀取任意其他的命名子產品,無論自動還是顯示:
(這些新的可讀性邊确實在子產品圖中造成了回路,使得它有些更加難懂了,但是我們把這看作是更加靈活遷移的可容忍的結果。)
類似的,沒有好的辦法去判斷一個自動子產品中的包會被其他子產品或者仍在類路徑中的類使用。是以,自動子產品中的每個包都會被導出,即使實際上它隻被内部使用:
最後,沒有好的辦法判斷自動子產品中是否有導出包中包含某些類型,它的方法簽名中引用了其他自動子產品中的類型。例如,我們首先子產品化com.foo.app。并且将com.foo.bar和org.baz.qux都當作自動子產品,那麼我們将會得到下面子產品圖:
不讀取相關的JAR檔案中的所有類檔案,是不能知道com.foo.bar中的公共類型是否聲明了一個傳回類型是org.baz.qux中的公共方法。是以,自動子產品被授予對其它所有自動子產品的隐式可讀性:
現在,com.foo.app中的代碼可以通路org.baz.qux中的類型,盡管我們知道實際上并不是這麼做的。
自動子產品提供了一個類路徑的混亂與顯示子產品的嚴格的中間方案。就像上面看到的,他們允許一個由JAR檔案組成的現有應用可以以自上而下,或者結合自上而下與自下而上的方式遷移到子產品化中來。一般來講,我們從一組任意類路徑下的JAR檔案元件開始,使用jdeps工具來分析他們的互相依賴,将那些我們可以控制源碼的元件轉換為顯示子產品,并且與剩餘的JAR檔案一起放到子產品路徑下。那些不能控制源碼的JAR檔案組建會被當作自動子產品直到有一天他們也被轉換為顯示子產品。
3.4 類路徑橋接
許多現存的JAR檔案可以被用作自動子產品,但是有些卻不能。如果類路徑下有多于兩個JAR檔案有相同的包,那麼最多隻有一個可以被用作自動子產品,因為子產品系統要確定每個命名子產品最多讀取一個定義了特定包的子產品,以及定義相同包的命名子產品不會互相幹擾。在這種情況下,通常隻需要一個JAR檔案。如果其他重複或者近似重複的,不小心放到了類路徑下,那麼可以将一個用作自動子產品并且丢棄其他的。然而如果類路徑上的多個JAR檔案有意地包含相同包中的類型,那麼他們必須被保留在類路徑上。
為了能夠在多個JAR檔案無法被用作自動子產品時依舊可以遷移,我們可以用自動子產品作為顯示子產品中的代碼與類路徑中的代碼的橋梁:除了讀取所有的命名子產品,自動子產品還會讀取未命名子產品。例如,如果我們應用的原始類路徑包含了org-baz-fiz.jar跟org-baz-fuz.jar檔案,那麼我們得到下圖:
就像前面提到的,未命名子產品會導出它的所有包,是以自動子產品中的代碼可以通路類路徑中加載的所有公共類型。
使用類路徑中某類型的自動子產品必須不能将這些類型暴露給依賴它的顯示子產品,因為顯示子產品不能聲明對未命名子產品的依賴。例如,如果顯示子產品com.foo.app中的代碼引用了com.foo.bar中的公共類型,并且該類型的方法簽名引用了依舊在類路徑JAR檔案中的類型,那麼com.foo.app中的代碼将無法通路這些類型,因為com.foo.app不能依賴未命名子產品。可以臨時将com.foo.app當作自動子產品來補救這一點,以便它的代碼可以通路類路徑中的類,直到類路徑中的相關JAR檔案可以被當作自動子產品或者轉換為顯示子產品。
4 服務
通過服務接口與服務提供者實作程式元件間的松耦合是大型軟體系統的強大工具。Java一直通過java.util.ServiceLoader來支援服務,它在運作時通過搜尋類路徑來定位服務提供者。對于子產品中定義的服務提供者,我們必須考慮如何在諸多可觀察子產品中定位這些子產品來解決它們之間的依賴,并且使得那些使用相應服務的代碼可用。
例如,假設我們的com.foo.app子產品使用MySQL資料庫,并且MySQL 的JDBC驅動是在如下可觀察子產品中提供的:
module com.mysql.jdbc {
requires java.sql;
requires org.slf4j;
exports com.mysql.jdbc;
}
其中org.slf4j是驅動中使用的日志庫,com.mysql.jdbc是包含java.sql.Driver服務接口實作的包。(沒有必要導出驅動包,這裡隻是為了清晰)
為了使java.sql子產品可以使用該驅動,ServiceLoader類必須可以通過反射執行個體化該驅動類;為了實作這一點,子產品系統必須将驅動子產品添加到子產品圖中并解決它的依賴,是以:
為了完成這點,子產品系統必須可以通過之前解析的子產品來識别任何服務的使用,然後從諸多可觀察子產品中定位并解析提供者。
子產品系統可以通過掃描子產品工件中的類檔案來調用ServiceLoader::load 方法來識别服務的使用,但是這樣即慢又不可靠。一個子產品使用特定的服務是該子產品定義的基本面,是以為了效率和清晰度,我們在子產品定義中使用
use
語句表達這一點:
module java.sql {
requires public java.logging;
requires public java.xml;
exports java.sql;
exports javax.sql;
exports javax.transaction.xa;
uses java.sql.Driver;
}
就像ServiceLoader類目前所做的一樣,子產品系統可以通過掃描子產品工件的META-INF/services資源條目來識别服務提供者。一個子產品提供一個特定服務的實作同樣的重要,是以我們在子產品聲明中使用
provides
語句來表達這一點:
module com.mysql.jdbc {
requires java.sql;
requires org.slf4j;
exports com.mysql.jdbc;
provides java.sql.Driver with com.mysql.jdbc.Driver;
}
現在,通過簡單的閱讀這些子產品的聲明可以看到其中一個使用了另一個提供的服務。
在子產品聲明中聲明子產品提供與子產品使用關系不僅僅是提供效率跟清晰度。 這兩種服務聲明可以在編譯期被解析來確定服務接口(如java.sql.Driver)可以被提供者及服務使用者通路到。服務提供者聲明可以被進一步解析來確定提供者(如com.mysql.jdbc.Driver)确實實作了聲明的服務接口。最後,可以使用預先編譯和連結工具來解析服務使用聲明以確定可觀察的提供者在運作之前被正确的編譯和連結。
出于遷移的目的,如果定義自動子產品的JAR檔案包含META-INF/services資源條目,那麼每個條目都會被假設在該子產品下聲明了相應的provides語句。自動子產品可以使用任意的可用服務。
5 進階話題
本文檔的剩餘部分涉及到的進階話題,雖然重要,不過大部分開發人員可能并不感興趣。
5.1 映射
為了使子產品圖在運作時通過反射可用,我們在java.lang.reflect包中定義了Module類,并在java.lang.module包中新定義了一些相關類型。Module類的執行個體代表運作時的單個子產品。每個類型都在某個子產品中,是以每個Class對象都有一個相關聯的Module對象,可以通過Class::getModule方法傳回。
子產品對象的基本操作如下:
package java.lang.reflect;
public final class Module {
public String getName();
public ModuleDescriptor getDescriptor();
public ClassLoader getClassLoader();
public boolean canRead(Module target);
public boolean isExported(String packageName);
}
其中ModuleDescriptor是java.lang.module包中的類,它的執行個體表示子產品描述符;getClassLoder方法傳回子產品的類加載器;canRead方法判斷該子產品是否可以讀取目标子產品;以及isExported方法判斷指定的包是否被子產品導出。
java.lang.reflect包不是平台唯一的反射工具。編譯期javax.lang.model包中會有類似的添加,以支援注解處理器以及文檔工具。
5.2 映射可讀性
架構是一種在運作時使用反射來加載,檢查,執行個體化其他類的工具。Java SE平台自身的架構例子包括服務加載器,資源束,動态代理,以及序列化,當然還有很多流行的外部架構庫,像是資料庫持久,依賴注入和測試。
運作時發現的類,架構必須能夠通路它的某個構造器才能執行個體化它。然而事情并不總是這樣。
例如,平台的streaming XML parser,通過javax.xml.steam.XMLInputFactory系統屬性來加載并執行個體化XMLInputFactor服務的實作,如果定義了,則優先于通過ServiceLoader類來發現發現提供者。忽略異常處理和安全檢查,代碼大緻像這樣:
String providerName
= System.getProperty("javax.xml.stream.XMLInputFactory");
if (providerName != null) {
Class providerClass = Class.forName(providerName, false,
Thread.getContextClassLoader());
Object ob = providerClass.newInstance();
return (XMLInputFactory)ob;
}
// Otherwise use ServiceLoader
...
在子產品化環境下,隻要包包含對context類加載器可知的提供者類,那麼對Class:forName的調用就可以正常工作。然而,通過反射newInstance方法對提供者類構造器的調用就沒那麼幸運了。提供者可能是從類路徑中加載的,這種情況下它位于未命名子產品内,或者一些明明子產品内,但是不管哪種情況,架構自身是在java.xml子產品中的。該子產品僅僅依賴于基子產品,是以其他子產品中的提供者對架構來說是不可通路到的。
為了使提供者類對架構是可通路的,我們需要讓提供者子產品對架構子產品是可讀的。我們可以允許所有架構顯示的将必要的可讀性邊在運作時動态的添加到子產品圖中來,就像本文檔之前的版本描述的一樣,但是經驗告訴我們,這種方式太麻煩,并且礙于遷移。
是以,取而代之,我們簡單的修訂了反射API,基于這樣一種假設:任何反射某些類型的代碼都在能夠讀取這些類型所在子產品的子產品中。這使得上面的例子以及跟此例相同的代碼可用,并且不需要做任何改動。這種方式并不會削弱強封裝性:如果公共類型想要被其他子產品通路,無論是從編譯的代碼中,還是通過反射,它都必須在子產品的導出包中。
5.3 類加載器
每個類型都在某個子產品中,并且運作時每個子產品都有一個類加載器,但是一個類加載隻加載一個子產品嗎?事實上,子產品系統在子產品與類加載器之間存在少量限制。一個類加載器可以一個或多個子產品中的類型,隻要子產品間不互相幹擾,并且子產品中的所有類型必須是由同一個加載器加載的。
這種靈活性對相容性極其重要,因為這允許我們保留平台現存的内置類加載器的層次結構。bootstrap和extension類加載器依舊存在,并且用來加載平台子產品中的類型。application類加載器也存在,并且用來加載子產品路徑中的類型。
這種靈活性還可以使現有的應用程式更容易地子產品化,這些應用程式已經建構了自定義類加載器的複雜層次結構或甚至圖,因為可以更新加載器用來加載子產品中的類型,而不必改變其委托模式。
5.4 未命名子產品
我們前面學到如果一個類型不是定在在命名的,可觀察的子產品中,那麼它就是未命名子產品中的成員,但是這個未命名子產品所關聯的類加載器是哪個呢?
事實上,每個類加載器都有唯一的未命名子產品,可以通過ClassLoader::getUnnamedModule新方法獲得。如果一個類加載器加載了一個未命名子產品中的類型,那麼這個類型會被認在該類加載器的未命名子產品中,例如,該類型的Class對象的getModule方法會傳回它的類加載器的未命名子產品。是以,被簡稱為“未命名子產品”的子產品其實是application類加載器的未命名子產品,它從類路徑加載那些沒有定義在已知子產品中的類型。
5.5 層
子產品系統不會強制規定子產品與類加載器之前的關系,但是為了加載特定的類型,必須能夠以某種方式找到一個合适的加載器。是以,子產品圖在運作時的執行個體化會産生一個
層
,它将圖中每個子產品映射到唯一的負責加載該子產品中類型的類加載器。就像前面讨論的,
boot
層是JVM在啟動時通過解析應用的初始子產品,而不是可觀察子產品來建立的。
大部分的應用,以及現存的所有應用将不會用到除了boot層以外的其它層。然而複雜應用可以通過插件或者容器架構使用多層,像應用伺服器,IDE,以及測試工具。這些應用可以使用動态的類加載以及子產品系統反射API,加載和運作托管的應用,這些應用包含一個或多個子產品。然而,還需要額外的兩個靈活性:
- 某個托管應用可能需要某個已經存在的子產品的不同版本。例如,一個Java EE的web應用,可能需要JAX-WS棧在java.xml.ws子產品中的版本,而不是運作時環境中内置的版本。
- 某個托管應用需要的服務提供者可能不是已經發現的提供者。托管系統甚至會嵌入自己傾向的提供者。例如,一個web應用可能包含Woodstox streaming XML parse版本的一個副本,這種情況下,ServiceLoader類應該傳回這個提供者,而不是其它的。
容器應用可以在現存層的上面為托管應用建立一個新層,通過解析應用的初始子產品而不是一個可觀察子產品的不同空間。這個空間可以包含可平台可更新子產品或者其他子產品的備用版本,非平台子產品存在于更低的層中;解析器會為這些備用子產品設定優先級。這樣一個空間也可以包含不同的服務提供者而不是哪些已經在低層被發現的;ServiceLoader類會在從低層傳回提供者之前加載并且傳回相應的提供者。
層可以疊加:一個新層可以建構在boot層上,并且其它層也可以建構在它之上。正常的解析過程的結果是某一層上的子產品可以讀取位于該層以及低于該層中的子產品。是以某一層的子產品圖可以按引用包含比它低的每一層的子產品圖。
5.6 有限制的導出
有時候有必要重新安排某些類型,使其對于一些子產品是可通路的,而對其他的所有子產品是不可通路的。
例如,标準JDK實作java.sql跟java.xml子產品中的代碼使用了定義在内部包sun.reflect中的類型,該包位于java.base子產品中。為了使這些代碼可以通路sun.reflect包中的類型,我們可以簡單在java.base子產品中導出該包:
module java.base {
...
exports sun.reflect;
}
然而這使得sun.reflect包中的所有類型對于其它任意子產品都是可以通路的了,因為所有子產品都可以讀取java.base子產品,這不是我們想要的結果,因為該包中的一些類定義了授權,安全敏感的方法。
因為我們拓展子產品定義來允許包被導出給一個或多個特定名稱的子產品,而不會導出給除此之外的其他子產品。java.base子產品的聲明事實上隻把sun.reflect包導出給了特定的幾個JDK子產品:
module java.base {
...
exports sun.reflect to
java.corba,
java.logging,
java.sql,
java.sql.rowset,
jdk.scripting.nashorn;
}
這種
有限制的導出
可以在子產品圖中以另一種類型的邊呈現,這裡是金色的,從包到它所導出的指定的子產品:
下面精煉了上面陳述的可通路性規則:兩個類型S和T被定義在不同的子產品中,并且T是公共的,那麼S中的代碼可以通路T的條件是:
- S的子產品可以讀取T的子產品,并且
- T的子產品導出T所在的包,到S所在的子產品,或者所有子產品。
同樣我們拓展反射Module類添加一個方法來判斷是否某個包被導出給了特定的子產品,而不是所有子產品:
public final class Module {
...
public boolean isExported(String packageName, Module target);
}