JDK9的釋出一直在推遲,終于在2017年9月21日釋出了。下面是JDK9的幾個下載下傳位址:
JDK9.0.1 Windows-x64下載下傳位址
Oracle Java 官網下載下傳位址
OpenJDK 9官網
OpenJDK JDK9下載下傳
從安裝的JDK9檔案夾下會發現沒有jre檔案夾了,并且多了一個jmods檔案夾,想想為什麼?
傳統的jar檔案是在運作時runtime使用,而 .jmods檔案是在開發時development time使用。
這一次,Java9帶來的子產品化(Modularity)是一次重大的改變。對于在此之前(Java8及以前)的Java釋出版本所添加的新特性,你都可以随意使用,但是Java9不同,這次Java9的平台子產品化系統(Java Platform Modular System)在思考、設計、編寫Java應用程式方面都是一個全新的改變。
1.Java 9 子產品化簡介
子產品化是Java9釋出版本最重要最強大的改變,此外Java9還帶來了許多新的改變,例如支援HTTP2.0、互動式Shell(叫做jshell)等。那麼子產品化究竟會帶來什麼好處呢?為何要引入子產品化?
子產品(module)可以是任意東西,從一組代碼實體、元件或UI類型到架構元素再到完整的可重用的庫。
子產品化在軟體開發中通常要到達兩個目标:
- 分而治之(Divide and conquer approach):對于非常大的問題通常需要将大問題分解成一個個的小問題,然後單獨解決它們。
- 實作具有封裝性和明确定義的接口:子產品化後就可以隐藏子產品的内部實作(稱為封裝encapsulation),同時暴露給使用者的東西稱為接口(interface)。
現在回顧下封裝:
private 修飾成員變量和方法,封裝的邊界是Class(類)。
protected 修飾成員變量和方法,封裝的邊界是Package(包)。
無修飾符 修飾成員變量和方法或類型(Types),封裝的邊界是Package(包)。
對于封裝,難道有這些還不夠嗎?上面這些修飾符都集中在控制通路成員變量和方法上面。而對于類型(types)的通路保護(封裝)隻能讓它在包層級保護package-protected。子產品化可以在更大的粒度上進行封裝,對類型的保護變成private。
來看幾個沒有子產品化帶來的問題的案例:
1.1 無法隐藏内部API和類型
為了更好的重用一個字元串排序的工具類,将它打包成一個jar檔案,它又兩個包組成:acme.util.stringsorter和acme.util.stringsorter.internal。
前者包含隻有一個方法sortStrings(List)的類StringSorterUtil,後者包含隻有一個方法sortStrings(List)的BubbleSortUtil類,BubbleSortUtil類用的是著名的Bubble排序算法對給定的字元串排序,調用StringSorterUtil的sortStrings方法實際上是反向代理執行BubbleSortUtil類的sortStrings方法。

後來jar包開發者發現哈希Hash排序算法更優于Bubble排序算法,于是更新了下,将HashSortUtil類加到了acme.util.stringsorter.internal包下面并移除掉了原來的BubbleSortUtil類。幸好單獨有這麼個internal包,使用者調用StringSorterUtil的sortStrings方法的方式沒有改變,那使用者就可以直接更新這個jar包了。一切是多麼的美好。
但是!還是出問題了。
jar包作者本意是acme.util.stringsorter.internal包不讓使用者使用,是private的,但是當使用者将這個jar包加到classpath後,仍然可以直接使用BubbleSortUtil類,這并不是jar包開發者所希望的。現在更新了版本後,那些直接使用BubbleSortUtil類的應用由于找不到BubbleSortUtil類連編譯都通不過。顯然,即使将包命名為internal還是無法避免使用者去通路它。
Java平台内部API是不建議使用的,盡管官方給出了提醒,但還是無法避免開發者使用,現在在Java9中已經将其隐藏了,例如以sun開頭的包。
那麼有沒有什麼方式來封裝這些internal類呢?子產品!
1.2 可靠性問題
應用啟動運作了幾個小時候沒有發生錯誤,但是,并不能說之後就沒有問題。比如或許有一個類還沒有被執行到,當執行到它時,JVM發現找不到這個類的一個import,抛出類找不到異常。又或許同一個類的多個版本加到了類路徑而JVM隻選擇了它找到的第一個副本。難道沒有一個更好的方式來確定任意的Java應用不需要執行就将會可靠reliably地運作?子產品描述符!
1.3 類路徑classpath問題
Jar檔案僅僅是将一組類友善的放在一起而已。一旦加入到classpath中,JVM就對Jar中的所有classes一視同仁放到同一個根root目錄下,而不管這些class檔案位置在哪。想象一個應用的成千上萬的類放置在同一個目錄下而沒有結構的樣子,這對于管理和維護将是一場噩夢。代碼庫越大,問題越大。例如有20悠久曆史的Java平台本身!!!
Java 1996年釋出的第一個版本至少有500個public類,到2014年釋出的JDK8已經達到4200多個public類和20000多個檔案。傳統地,每一個JRE在運作時都要從一個庫加載所有的類,這個庫就是rt.jar,rt的意思是Run Time.
Java9之前,每一個runtime自帶開箱即用的所有編譯好的平台類,這些類被一起打包到一個JRE檔案叫做rt.jar。你隻需将你的應用的類放到classpath中,這樣runtime就可以找到,而其它的平台類它就簡單粗暴的從rt.jar檔案中去找。盡管你的應用隻用到了這個龐大的rt.jar的一部分,這對JVM管理來說不僅增加了非必要類的體積,還增加了性能負載。
Java8的rt.jar大概 60 MB大小,目前尚可忍受,但如果一直這樣下去想象之後它的體積肯定會越來越龐大,難道沒有更好的方式來運作java嗎?Java9子產品化可以按需自定義runtime!這也就是jdk9檔案夾下沒有了jre目錄的原因!
1.4 Java Platform Module System(JPMS)
JPMS(JAVA平台子產品化系統)引入了一個新的語言結構來建構可重用的元件,稱為子產品modules。在Java9 的子產品中,你可以将某些類型types和包packages組合到一個子產品module中,并給子產品提供如下3個資訊:
- 名稱:子產品的唯一的名字,例如 com.acme.analytics,類似于包名。
- 輸入:什麼是子產品需要和使用到的?什麼是子產品編譯和運作所必需的?
- 輸出:什麼是子產品要輸出或暴露給其他子產品的?
預設地,一個子產品中的每一個java類型隻能被該子產品中的其他類型所通路。要想暴露類型給外部的子產品使用,需要明确指定哪些包packages要暴露export。任何子產品隻能在包的層級上暴露,一旦暴露了某個包,那這個包中的所有的類型就都可以被外部子產品通路。如果一個Java類型所在的包沒有暴露,那麼外部其他子產品是無法import它的,即使這個類型是public的。
JPMS具有兩個重要的目标,要牢記:
- 強封裝Strong encapsulation:由于每一個子產品都聲明了哪些包是公開的哪些包是内部的,java編譯和運作時就可以實施這些規則來確定外部子產品無法使用内部類型。
-
可靠配置Reliable configuration:由于每一子產品都聲明了哪些是它所需的,那麼在運作時就可以檢查它所需的所有子產品在應用啟動運作前是否都有。
除了上面兩個核心的目标,JPMS還有另外一個重要的目标是易于擴充和使用即使在龐大的類庫上。
是以對Java平台自身進行了子產品化,實施于項目Project Jigsaw
1.5 Project Jigsaw
Modular development starts with a modular platform. —Alan Bateman 2016.9
子產品化開發始于子產品化的平台。
要寫子產品化代碼,需要将Java平台子產品化。Java9之前,JDK中所有的類都糅雜在一起,像一碗意大利面。這使得JDK代碼庫很難改變和發展。
Java 9 子產品化JDK如下圖:
Project Jigsaw 有如下幾個目标:
- 可伸縮平台Scalable platform:逐漸從一個龐大的運作時平台到有有能力縮小到更小的計算機裝置。
- 安全性和可維護性Security and maintainability:更好的組織了平台代碼使得更好維護。隐藏内部APIs和更明确的接口定義提升了平台的安全性。
- 提升應用程式性能Improved application performance:隻有必須的運作時runtimes的更小的平台可以帶來更快的性能。
- 更簡單的開發體驗Easier developer experience:子產品系統與子產品平台的結合使得開發者更容易建構應用和庫。
子產品化的另外一個重要的方面是版本控制versioning。現在的JPMS不支援versioning!!!
在控制台輸入如下指令可以檢視所有的子產品:java –list-modules
檢視某個子產品(例如java.sql)的詳情(描述符)使用–describe-module或-d:
java –describe-module java.sql
$ java -d java.sql
java.sql@9
exports java.sql
exports javax.sql
exports javax.transaction.xa
requires java.base mandated
requires java.logging transitive
requires java.xml transitive
uses java.sql.Driver
從Java平台的子產品描述符中可以看出有幾個關鍵詞requires(輸入)、exports(輸出)、users(使用服務:消費者)、providers(提供服務:服務實作者)、transtive(傳遞性)
java.se 是Java SE包含的所有子產品:
java.base是最基礎的子產品,也是java開發所需要的最小的子產品,沒有它就寫不了java代碼。是以該子產品會自動隐含地加入所有子產品(即所有子產品描述符會隐含這條語句requires java.base),是以子產品描述符中不需要明确的requires。
java.xml 是與xml有關的類型的子產品。
……
從用–list-modules指令檢視所有的子產品的字首中(例如java、javafx、jdk)可以看出一些規律:
- java :指核心的Java平台子產品。即官方的标準子產品。
- javafx : 指Java FX子產品,即用于建構桌面應用的平台子產品。
- jdk:指核心的JDK子產品。這些不是Java語言規範的一部分,但包含了一些有價值的工具和APIs。
- oracle:如果下載下傳的是Oracle Open JDK,就可以看到一些已oracle為字首的子產品。不建議使用。
以java.為字首的子產品又可以分為3大類:
- 核心Java子產品:指核心的Java SE APIs,例如java.bas,java.xml。
- 企業級子產品:包含一些如java.corba(包含遺留CORBA技術)和java.transaction(提供資料庫事務APIs)。注意它與Java EE不同,Java EE是一個完全不同的規範。Jave SE和Java EE有一些重疊的地方,為了避免這些重疊,在Java9中已經将這些企業級子產品标記為廢棄,在将來的版本中可能會被移除掉。
- 聚合(Aggregator)子產品:這些子產品本身沒有包含任何API,而是作為一種非常簡便的方式将多個子產品綁在一起。目前平台有兩個聚合子產品java.se以及java.se.ee(JavaSe加上與JavaEE重疊的部分)。聚合子產品一般是将核心的子產品組合在一起使用,要小心使用。
2.建構第一個Java子產品Module
首先,需要下載下傳和安裝Java 9 SDK,即JDK 9,下載下傳連結在文章開頭已經給出,推薦第一個連結。
為了驗證安裝和配置是否正确,打開指令行視窗,輸入java -version和echo %JAVA_HOME%指令。
下面用任意的一個文本編輯器開發第一個子產品應用,暫時先不用IDE。
2.1建立一個子產品
- 給子產品取個名字:例如com.acme.stringutil
- 建立一個子產品根檔案夾:根檔案夾的名稱和子產品名一樣為com.acme.stringutil
- 添加子產品代碼:如果子產品有一個類StringUtil.java位于com.acme.util包下面,那麼檔案夾的結構則如下圖所示:
Java 9 子產品化(Modularity)
完整的目錄結構如下:
4.建立和配置子產品描述符:每一個子產品都有一個檔案用于描述這個子產品包含的中繼資料。這個檔案叫做子產品描述符module descriptor。這個檔案包含了這個子產品的資訊,如輸入輸出。通常這個檔案直接位于子產品的根檔案夾下,通常取名為 module-info.java.下面是這個檔案的最小的配置内容:
module com.acme.stringutil {
}
注意該描述符中雖然沒有任何内容,但是隐含的requries java.base子產品。
2.2 建立第一個子產品
通常一個應用會包含很多個子產品,先建立一個子產品叫packt.addressbook。
接下來需要建立一個Java檔案叫Main.java,放置在packt.addressbook包中,其完整路徑為:
~/code/java9/src/packt.addressbook/packt/addressbook/Main.java
Main.java内容如下:
package packt.addressbook;
public class Main{
public static void main(String[] args){
System.out.println("Hello World!");
}
}
最後建立一個子產品描述符檔案module-info.java,将它直接放在子產品根目錄下。到此就完成了:
2.3 編譯子產品
編譯子產品需要用到javac指令。(確定JAVE_HOME和path已經配置好了)goto 到項目根目錄下 ~/code/java9 輸入指令:
當編譯成功後,控制台沒有輸入。out目錄應該包含編譯好的類:
2.4 執行子產品
執行上一步編譯好的代碼,需要在相同的目錄~/code/java9下運作如下指令:
java --module-path out --module packt.addressbook/packt.addressbook.Main
–module-path可以用-p代替,–module可以用-m代替
如果執行成功,可以在控制台看到Hello World!
2.5 建立第二個子產品
為了示例多個子產品,将上面的應用分解為兩個子產品。
接下來建立第二個子產品,然後讓上面第一個子產品使用第二個子產品。
- 建立一個新的子產品,命名為: packt.sortutil.
- 将排序相關的代碼移到新的子產品packt.sortutil中。
- 配置packt.sortutil子產品描述符(輸入輸出是什麼)。
-
配置 packt.addressbook 依賴新子產品packt.sortutil。
下面是檔案夾結構:
Java 9 子產品化(Modularity)
其中packt.sortutil的子產品描述符module-info.java:
module packt.sortutil {
exports packt.util;
}
packt.addressbook的子產品描述符module-info.java:
module packt.addressbook {
requires packt.sortutil;
}
這樣packt.sortutil子產品可以當做庫來使用了,但需要注意到的是,當你建立了一個庫,在你允許其他人使用它時,你要非常謹慎地定義你的庫的API。原因是一旦其他人開始使用你的庫,就很難去對庫的public API做改變。将來版本的對API的任何改變都意味着需要你的庫的所有使用者要更新他們的代碼來時新的API有效。
是以盡量使SortUtil類輕量點,讓它作為packt.sortutil庫的接口。是以可以将實際的排序邏輯放到一個實作類中,例如建立一個實作類BubbleSortUtilImpl.java:
public class BubbleSortUtilImpl {
public <T extends Comparable> List<T> sortList(List<T> list) {
...
}
private <T> void swap(List<T>list, int inner) {
...
}
}
然後SortUtil.java類可以很簡單的代理執行排序方法:
public class SortUtil {
private BubbleSortUtilImpl sortImpl = new BubbleSortUtilImpl();
public <T extends Comparable> List<T> sortList(List<T> list) {
return this.sortImpl.sortList(list);
}
}
子產品結構如下:
由于實作類BubbleSortUtilImpl.java放到一個新的包,是以對外是隐藏的,也就是說外部子產品是無法直接使用BubbleSortUtilImpl類的。這是不是解決了Java9之前無法隐藏内部類型的問題了呢?
注意java中的包是沒有遞階控制的not hierarchical。包packt.util和包packt.util.impl是兩個獨立的包,二者毫無關系。暴露packt.util包并沒有将packt.util.impl暴露。
編譯:javac -d out –module-source-path src –module packt.addressbook,packt.sortutil
執行:java –module-path out -m packt.addressbook/packt.addressbook.Main
3.子產品概念Module Resolution, Readability, and Accessibility
Java9子產品化有3個概念非常重要,子產品解析、可讀性、可通路性。
3.1 Readability 可讀性
當一個子產品依賴另一個子產品時,即第一個子產品read讀第二個子產品。也就是說第二個子產品可以被第一個子產品讀readable。用圖表示就是第一個子產品箭頭指向第二個子產品。假設有下面3個子產品,關系如下:
子產品A requiers B。是以子產品A reads B。子產品B is readable by A。同理子產品C reads B。
然而子產品A does not read C ,反之亦然。
你會發現上面的關系是非對稱的。事實上,在Java子產品系統中,是可以保證子產品間的關系絕對是非對稱asymmetric的。why?因為如果兩個子產品可以互相read,那麼它們會形成一個循環依賴,這是平台所不允許的。是以一個子產品requires第二個子產品,那第二個子產品必定不能requires第一個子產品。
一個特殊的子產品是java.base,每一個子產品首先都會read它。這個依賴是自動完成的而且并不需要顯示地requires。
可讀性readability關系是最基礎的,它實作了Java子產品系統兩個主要目标之一,即可靠性配置reliable configuration。
3.2 Accessibility 可得性
Accessibility 是Java子產品的另一性質。如果可讀性readability關系表明了某個子產品可以讀read哪些子產品,那麼accessibility表明這個子產品可以實際從中讀取什麼。一個子產品被其他子產品read時,中并不是所有的東西都可以accessible,隻有那些使用export标記的包中的public類型才可以被通路到。
是以,對于子產品B中的一個類型type(可以是interface、class等)能夠被子產品A通路到,需要同時滿足一下幾個條件:
- 子產品A需要 read B
- 子產品B需要暴露export包含了該類型的包
- 該類型本身是public的
3.2.1 接口實作accessibility
我們可以考慮在LibApi接口中添加一個static方法來建立一個LibApiImpl類的執行個體。它的傳回類型是LibApi,這一點很重要。
package packt.lib.external;
public interface LibApi {
static LibApi createInstance() {
return new LibApiImpl();
}
public void testMethod();
}
然後建構一個簡單的實作類LibApiImpl實作LibApi接口,注意在類前沒有關鍵字public修飾符。這意味着這個類是包所有package-private,不是public。即使它與LibApi在同一個包中被子產品暴露export,它仍然是不可被外部子產品通路到的。
package packt.lib.external;
class LibApiImpl implements LibApi {
public void testMethod() {
System.out.println("Test method executed");
}
}
外部子產品就可以這樣使用它:
package packt.app;
import packt.lib.external.LibApi;
public class App {
public static void main(String[] args) {
LibApi api = LibApi.createInstance();
api.testMethod();
}
}
這種設計模式是非常有價值的,因為它可以讓你在重寫底層的實作時不用改變提供的public APIs。當然讓實作類放到另一個包中不讓它暴露也可實作同樣的效果。
3.2.2 Split packages 分離包
上面外部子產品中的包packt.app中的類App無法直接通路到包packt.lib.external中的LibApiImpl類,你可能會想如果類App放在外部子產品的一個相同名的包packt.lib.external中,那是否可以通路LibApiImpl呢?這當然行不通,在編譯時就發生錯誤:package exists in another module。
是的!同一包名不能同時存在于兩個子產品中。至少不能存在于兩個可觀察到observable的子產品中。換句話說,一個應用的某個包,在子產品路徑上它隻能是唯一地屬于某個子產品。
傳統的類路徑上的多個Jar檔案是可以同時包含相同的包的。而子產品是不允許共享包(即Split packages 分離包)。
3.3 Implied readability 隐含的可讀性
下面來看一個依賴洩露的問題。假設有3個子產品依賴關系如下:
module A {
requires B;
}
module B {
requires C;
}
子產品A requires B,子產品B requires C 。目前為止我們知道A does not read C ,因為本質上子產品依賴性非傳遞性 not transitive的。但萬一我們需要呢?例如子產品B有一個API,它的傳回類型是子產品C中的。
有一個好的例子可以從Java平台自身中找到。比如你自己的子產品依賴了java.sql子產品。你就可以使用該子產品裡面的Driver接口了。這個Driver接口有一個方法叫getParentLogger(),它傳回Logger類型,改類型在java.logging子產品當中。定義如下:
Logger getParentLogger() throws SQLFeatureNotSupportedException
下面是你自己的子產品調用的代碼:
你在你的子產品描述符中隻添加requires java.sql語句,這樣就可以了嗎?下面看下依賴關系:
由于你自己的子產品并沒有直接require java.logging,為了使用java.sql子產品的API,你還得require java.logging 子產品!
那有沒有更好的方式呢?盡管預設下依賴不具有傳遞性,但有時我們想可以有選擇的讓某些依賴具有傳遞性。Java9有這麼個關鍵詞transitive(傳遞性)可以做到。使用方式
requires transitive <module-name>;
是以之前子產品A要可以通路到C,可以這麼做:
module A {
requires B;
}
module B {
requires transitive C;
}
現在子產品C不僅僅可以被B可讀,而且所有依賴B的子產品都可以讀C。這樣A可以讀C了。
子產品A沒有直接requires C,但通過transitive,可以讀到C,這種關系就是隐含的可讀性。
在指令行運作:java -d java.sql
$ java -d java.sql
module java.sql@9
exports java.sql
exports javax.sql
exports javax.transaction.xa
requires transitive java.logging
requires transitive java.xml
requires mandated java.base
uses java.sql.Driver
注意到有兩個子產品java.logging和java.xml都用transitive做了标記。這就意味着那些依賴于java.sql的子產品将自動地可以通路java.logging和java.xml。這是Java平台做出的決定,因為使用java.sql的APIs時也需要使用到其它兩個子產品。是以之前自己子產品隻依賴java.sql是沒有問題的,因為可以隐含的read子產品java.logging。
在你的子產品中添加傳遞性transitive 依賴需要十分謹慎。想象下你隻依賴的一個子產品但由于其使用了transitive 你卻無心地得到了幾十個其他的子產品依賴。這種子產品設計顯然是違背了子產品化的原則。是以除非萬不得已,千萬不要使用傳遞性transitive 。
然而,實際上有一個非常有趣和簡便的使用傳遞性依賴的方式,那就是聚合子產品 aggregator modules。
指令行運作:java -d java.se
$ java -d java.se
java.se@9
requires java.scripting transitive
requires java.xml transitive
requires java.management.rmi transitive
requires java.logging transitive
requires java.sql transitive
requires java.base mandated
...
我希望你可以拒絕使用聚合子產品的誘惑。你可以使用java.se聚合子產品,但這樣你就失去了子產品化的目的又回到了Java8及以前的模式(通過依賴整個平台APIs而不管你實際需要的其中的哪部分)。這個聚合子產品主要是用于遺留代碼的遷移的作用。
java.se.ee聚合子產品已經廢棄了并不贊成使用。
3.4 Qualified exports 限定輸出
上一節介紹了傳遞性依賴如何對readability可讀性關系稍作了調整,這一小節将介紹一種對accessibility關系稍作調整的方式。通過使用qualified exports。
考慮一下這樣一種需求:假如子產品B被A使用,那子產品B中的暴露的public類型就可以被A使用了,但B中某個私有包(沒有暴露)僅僅可以被子產品A使用,而不能被其他子產品所使用,那該怎麼做?
可以使用限定輸出:
exports <package-name> to <module1>, <module2>,... ;
module B {
exports moduleb.public; // Public access to every module that reads me
exports moduleb.privateA to A; // Exported only to module A
exports moduleb.privateC to C; // Exported only to module C
}
指令行運作 java -d java.base
module java.base@9
...
exports jdk.internal.ref to java.desktop, javafx.media
exports jdk.internal.math to java.desktop
exports sun.net.ext to jdk.net
exports jdk.internal.loader to java.desktop, java.logging,
java.instrument, jdk.jlink
要記住使用限定輸出通常是不推薦的。子產品化原則的推薦是一個子產品不應該被使用者所感覺到。限定輸出在一定程度上增加了兩個子產品的耦合度。除非萬不得已,不要使用限定輸出。
3.5 Services 服務
緊耦合tight coupling是指兩個實體高度依賴彼此以至于改變其中某個的行為時,需要調整實際的其中一個甚至二者的代碼。松耦合 loose coupling則與之相反,兩個實體沒有高度依賴,它們之間甚至不知道彼此的存在,但二者仍然可以互互相動。
那在Java子產品系統中兩個子產品的耦合是緊耦合還是松耦合呢?答案明顯是緊耦合。
來舉個例子。現在我們有一個排序的子產品叫 packt.sortutil。我們通過配置讓這個子產品暴露了一個接口以及封裝了一個實作。它隻有一個實作,所有其他子產品能做的隻是bubble排序。如果我們需要有多個排序子產品,讓消費者consumer子產品自己選擇其中的一個子產品去使用呢?
我們可以添加多個子產品提供不同的排序實作。但是,由于緊耦合,需要consumer子產品packt.addressbook不得不對每一個排序子產品都requires,盡管任何時候它可能隻會使用到其中的一個。有沒有一種接口作為隻讓消費者consumer子產品來依賴它就可以呢?有的!那就是Services!
Java開發者應該很熟悉一個概念–多态polymorphism。它從一個接口及其多個實作開始。讓我們定義一個服務接口叫MyServiceInterface.java:
package service.api;
public interface MyServiceInterface {
public void runService();
}
考慮到有3個接口的實作分别在不同的子產品,它們都要通路到MyServiceInterface接口來實作。MyServiceInterface 接口在子產品service.api中,如下圖所示:
現在consumer消費者子產品需要調用這些實作中的一個來運作服務。為了達到這個目标,不能讓消費者子產品直接read這些實作,因為這是緊耦合的。我們讓消費者子產品隻允許read接口子產品service.api.
3.5.1 The service registry 服務系統資料庫
為了跨過消費者子產品與實作者之間沒有緊耦合的橋,想象在二者直接有一個層叫做the service registry服務系統資料庫。服務系統資料庫是由子產品系統提供的一個層,用于記錄和注冊給定接口的實作作為服務。當消費者需要一個實作時,它就使用服務API來與服務系統資料庫交流,并獲得可用實作的執行個體。這樣就打破了provider和consumer的耦合度。接口是其他子產品所共享的公用實體。由于provider和consumer之間完全無法感覺彼此的存在,是以你可以任意的移除的其中的一個實作或者加入一個實作。那麼子產品是如何注冊登記register它們的實作呢?消費者子產品又是如何從系統資料庫registry中通路執行個體呢?下面來看實作的細節。
3.5.2 Creating and using services建立和使用服務
1.建立Java類型來定義服務:每一個服務可以是一個簡單的Java類型,它可以是接口、抽象類甚至是正常的類。讓接口作為服務通常比較理想。定義建立一個子產品,并在其中建立一個包含了該接口的包,并暴露它。例如子產品service.api 包含接口service.api.MyServiceInterface。
module service.api {
exports service.api;
}
2.建立一個或多個子產品都read接口子產品并實作接口。
3.讓實作子產品注冊它們自己作為服務提供者service providers:文法如下
provides <interface-type> with <implementation-type>;
例如:子產品service.implA 的實作類實作了MyServiceInterface接口,子產品描述符如下
module service.implA {
requires service.api;
provides service.api.MyServiceInterface with
packt.service.impla.MyServiceImplA;
}
4.讓消費者子產品注冊自己作為服務的一個消費者:使用關鍵詞users,文法是,
uses <interface-type>;
消費者子產品描述符:
module consumer {
requires service.api;
uses service.api.MyServiceInterface;
}
5.在消費者子產品中調用ServiceLoader API來通路提供者執行個體:由于沒有直接依賴,服務實作者完全無法感覺到消費者。是以無法實作new來執行個體化它。為了可以通路到所有已經注冊實作的提供者,你需要在消費者子產品中調用Java平台APIServiceLoader.load() 方法。
Iterable<MyServiceInterface> sortUtils =
ServiceLoader.load(MyServiceInterface.class);
這裡是依賴查詢dependency lookup,區分下Spring架構的依賴注入dependency injection。
上面隻是得到一個Iterable,顯然實際應用是需要一個選擇政策來選擇其中某個實作。例如排序接口SortUtil 可以定義一個根據集合大小來選擇哪個實作。
public interface SortUtil {
public <T extends Comparable> List<T> sortList(List<T> list);
public int getIdealMaxInputLength();
}
那麼它的實作者都以實作這個getIdealMaxInputLength接口,比如傳回4或者Integer.MAX_VALUE等等。
為了友善消費者使用,可以把選擇政策的邏輯放到排序接口SortUtil 中,可以利用接口靜态方法實作:
public interface SortUtil {
public <T extends Comparable> List<T> sortList(List<T> list);
public int getIdealMaxInputLength();
public static Iterable<SortUtil> getAllProviders() {
return ServiceLoader.load(SortUtil.class);
}
public static SortUtil getProviderInstance(int listSize) {
Iterable<SortUtil> sortUtils =ServiceLoader.load(SortUtil.class);
for (SortUtil sortUtil : sortUtils) {
if (listSize < sortUtil.getIdealMaxInputLength()) {
return sortUtil;
}
}
return null;
}
}
現在消費者子產品的Main方法就不用和ServiceLoader互動和循環查詢實作者執行個體:
SortUtil sortUtil = SortUtil.getProviderInstance(contacts.size());
sortUtil.sortList(contacts);
3.6 Understanding Linking and Using jlink
到目前為止,已經介紹了子產品化的幾個重要概念包括readability 、accessibility以及強大的services服務。這一小節将介紹應用開發的最後一個步驟–建構和打包應用。
3.6.1 Module resolution process 子產品解析過程
Java9之前,Java編譯器和Java運作時runtime會去尋找用于組成類路徑classpath的一些檔案夾和JAR檔案。這個類路徑是你可以在編譯階段可以傳給編譯器也可以在執行階段傳給運作時的配置選項。
而子產品則不同。我們不必再使用通常的類路徑了。由于每一個子產品都定義了它的輸入和輸出。現在就可以明确知道哪部分代碼是所需的。如下圖:
假設你要執行子產品C中的main方法,這最小的集合顯然是CBDA,而E是不需要的。而如果要執行E中的main方法,這次最小集合僅僅是ED,其他子產品可以忽略。為了得到哪些子產品是必需哪些是非必需的, 平台會運作一個過程來解析子產品,這個過程就叫做子產品解析過程module resolution process。
在圖論中,這個過程是指發現傳遞閉包,稱為有向無環圖directed acyclic graph 。
有一個指令行選項可以用來檢視子產品解析過程:–show-module-resolution
3.6.2 Linking using jlink
JDK9捆綁了一個新的工具叫 jlink,它可以讓你建構你自己的完整的運作時鏡像來運作你的應用。
jlink指令需要3個輸入,
- The module path:已經編譯好的子產品所在的路徑。多個路徑之間windows用分号分隔(Mac或Linux用冒号分隔)
- The starting module:解析過程從哪個子產品開始。可以是多個,用逗号分隔。
- The output directory:存放生成的鏡像的目錄。
文法如下:
jlink --module-path <module-path-locations>
--add-modules <starting-module-name>
--output <output_location>
記住子產品解析過程隻會識别requires語句。而服務Services是預設不會包含進去的,需要明确地加到–add-modules選項後面。另外一種簡便的方式是可以使用–bind-services選項。
這個連結步驟是可選的,它位于編譯階段和執行階段的中間。但是如果你将要使用jlink,你就有機會去做些優化,例如壓縮鏡像,确定和移除未使用到的類型等等。
3.6.3 Building a modular JAR file 建構一個子產品JAR檔案
通過使用jar指令:
$ jar --create --file out/contact.jar --module-version=
-C out/packt.contact
甚至有main方法的子產品也可以轉換成Jar檔案,例如:
$ jar --create --file out/addressbook-ui.jar --module-version=
--main-class=packt.addressbook.ui.Main -C out/packt.addressbook.ui
這樣可以直接使用java指令運作它。
4. 其他
- Optional dependencies 可選依賴:文法格式為,
限定符static告訴子產品系統跟在其後的子產品是可選的optional(運作時),也就是說在運作時,如果該子產品不可用,會出現一個NoClassDefFound錯誤,通常需要捕獲它。requires static <optional-module-dependency>;
- Optional dependencies using services 使用服務的可選依賴 : 将原來服務接口放到消費者中(不需要服務接口子產品),讓服務實作者依賴消費者子產品。這樣消費之子產品是無法感覺服務提供者,服務提供者是可選的Optional 消費者可以自己實作預設接口。
- Open modules for reflection 用于反射的開放子產品:現在由于子產品的強封裝性,所有封裝的類型是無法通過放射擷取到的,像使用者自定義的類型,那用到了反射的架構如Spring現在該如何掃描類型呢?為了解決這個問題,平台引入了一個概念叫開放子產品open modules.要讓整個子產品都open,隻需要在module關鍵字前面加上open關鍵字。例如:
。這樣子產品内容還是封裝的,但是在運作時它可以用反射所通路到。當然也可以隻對子產品中的某些包open,甚至可以讓某個包隻能被某個子產品通路,例如:open module <module-name> {}
module modulename { opens package.one; opens package.two to anothermodule; exports package.three; }
5.開發工具IDE
目前支援JDK9的開發工具有NetBeans和IntelliJ Idea,Elcipse尚在開發中,推薦使用NetBeans。注意如果官網的位址NetBeans官網釋出位址 不支援Java 9,那麼可以到下面位址下載下傳開發版本NetBeans開發版本位址
.
目前感覺Java8及之前的項目要想遷移到Java9有點麻煩,畢竟許多第三方的Jar包還沒有子產品化。是以在此不具體介紹代碼遷移。
相關下載下傳連結:
Modular Programming in Java 9_英文版.pdf
Java9子產品化程式設計_附帶源碼.zip
《道德經》第一章:
道可道,非常道。名可名,非常名。無名天地之始。有名萬物之母。故常無欲以觀其妙。常有欲以觀其徼。此兩者同出而異名,同謂之玄。玄之又玄,衆妙之門。
譯文:“道”如果可以用言語來表述,那它就是常“道”(“道”是可以用言語來表述的,它并非一般的“道”);“名”如果可以用文辭去命名,那它就是常“名”(“名”也是可以說明的,它并非普通的“名”)。“無”可以用來表述天地渾沌未開之際的狀況;而“有”,則是宇宙萬物産生之本原的命名。是以,要常從“無”中去觀察領悟“道”的奧妙;要常從“有”中去觀察體會“道”的端倪。無與有這兩者,來源相同而名稱相異,都可以稱之為玄妙、深遠。它不是一般的玄妙、深奧,而是玄妙又玄妙、深遠又深遠,是宇宙天地萬物之奧妙的總門(從“有名”的奧妙到達無形的奧妙,“道”是洞悉一切奧妙變化的門徑)。