1. 程式人生 > >Java 9 模組化(Modularity)

Java 9 模組化(Modularity)

從安裝的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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

從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 -versionecho %JAVA_HOME%命令。
下面用任意的一個文字編輯器開發第一個模組應用,暫時先不用IDE。

2.1建立一個模組

  1. 給模組取個名字:例如com.acme.stringutil
  2. 建立一個模組根資料夾:根資料夾的名稱和模組名一樣為com.acme.stringutil
  3. 新增模組程式碼:如果模組有一個類StringUtil.java位於com.acme.util包下面,那麼資料夾的結構則如下圖所示:
    這裡寫圖片描述

完整的目錄結構如下:
這裡寫圖片描述
4.建立和配置模組描述符:每一個模組都有一個檔案用於描述這個模組包含的元資料。這個檔案叫做模組描述符module descriptor。這個檔案包含了這個模組的資訊,如輸入輸出。通常這個檔案直接位於模組的根資料夾下,通常取名為 module-info.java.下面是這個檔案的最小的配置內容:

module com.acme.stringutil {
}
  • 1
  • 2

注意該描述符中雖然沒有任何內容,但是隱含的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!");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最後建立一個模組描述符檔案module-info.java,將它直接放在模組根目錄下。到此就完成了:
這裡寫圖片描述

2.3 編譯模組

編譯模組需要用到javac命令。(確保JAVE_HOME和path已經配置好了)goto 到專案根目錄下 ~/code/java9 輸入命令:

javac --module-source-path src -d out src/packt.addressbook/packt/addressbook/Main.java src/packt.addressbook/module-info.java
  • 1

這裡寫圖片描述

當編譯成功後,控制檯沒有輸入。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。

    下面是資料夾結構:
    這裡寫圖片描述

這裡寫圖片描述

其中packt.sortutil的模組描述符module-info.java:

module packt.sortutil {
    exports packt.util;
}
  • 1
  • 2
  • 3

packt.addressbook的模組描述符module-info.java:

module packt.addressbook {
    requires packt.sortutil;
}
  • 1
  • 2
  • 3

這樣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) {
    ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然後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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

模組結構如下:
這裡寫圖片描述
由於實現類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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然後構建一個簡單的實現類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");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

外部模組就可以這樣使用它:

package packt.app;
import packt.lib.external.LibApi;
public class App {
    public static void main(String[] args) {
        LibApi api = LibApi.createInstance();
        api.testMethod();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

這種設計模式是非常有價值的,因為它可以讓你在重寫底層的實現時不用改變提供的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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

模組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
  • 1

下面是你自己的模組呼叫的程式碼:

Logger myLogger = driver.getParentLogger();
  • 1

你在你的模組描述符中只新增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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

現在模組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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意到有兩個模組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
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我希望你可以拒絕使用聚合模組的誘惑。你可以使用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
}
  • 1
  • 2
  • 3
  • 4
  • 5

命令列執行 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

要記住使用限定輸出通常是不推薦的。模組化原則的推薦是一個模組不應該被使用者所感知到。限定輸出在一定程度上增加了兩個模組的耦合度。除非萬不得已,不要使用限定輸出。

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();
}
  • 1
  • 2
  • 3
  • 4

考慮到有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;
}
  • 1
  • 2
  • 3

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5

4.讓消費者模組註冊自己作為服務的一個消費者:使用關鍵詞users,語法是,uses <interface-type>;
消費者模組描述符:

module consumer {
    requires service.api;
    uses service.api.MyServiceInterface;
}
  • 1
  • 2
  • 3
  • 4

5.在消費者模組中呼叫ServiceLoader API來訪問提供者例項:由於沒有直接依賴,服務實現者完全無法感知到消費者。因此無法實現new來例項化它。為了可以訪問到所有已經註冊實現的提供者,你需要在消費者模組中呼叫Java平臺APIServiceLoader.load() 方法。

Iterable<MyServiceInterface> sortUtils =
ServiceLoader.load(MyServiceInterface.class);
  • 1
  • 2

這裡是依賴查詢dependency lookup,區分下Spring框架的依賴注入dependency injection。
上面只是得到一個Iterable,顯然實際應用是需要一個選擇策略來選擇其中某個實現。例如排序介面SortUtil 可以定義一個根據集合大小來選擇哪個實現。

public interface SortUtil {
    public <T extends Comparable> List<T> sortList(List<T> list);
    public int getIdealMaxInputLength();
}
  • 1
  • 2
  • 3
  • 4

那麼它的實現者都以實現這個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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

現在消費者模組的Main方法就不用和ServiceLoader互動和迴圈查詢實現者例項:

SortUtil sortUtil = SortUtil.getProviderInstance(contacts.size());
sortUtil.sortList(contacts);
  • 1
  • 2

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>
  • 1
  • 2
  • 3

記住模組解析過程只會識別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=1.0
-C out/packt.contact
  • 1
  • 2

甚至有main方法的模組也可以轉換成Jar檔案,例如:

$ jar --create --file out/addressbook-ui.jar --module-version=1.0
--main-class=packt.addressbook.ui.Main -C out/packt.addressbook.ui 
  • 1
  • 2

這樣可以直接使用java命令執行它。

4. 其他

  • Optional dependencies 可選依賴:語法格式為,requires static <optional-module-dependency>;限定符static告訴模組系統跟在其後的模組是可選的optional(執行時),也就是說在執行時,如果該模組不可用,會出現一個NoClassDefFound錯誤,通常需要捕獲它。
  • Optional dependencies using services 使用服務的可選依賴 : 將原來服務介面放到消費者中(不需要服務介面模組),讓服務實現者依賴消費者模組。這樣消費之模組是無法感知服務提供者,服務提供者是可選的Optional 消費者可以自己實現預設介面。
  • Open modules for reflection 用於反射的開放模組:現在由於模組的強封裝性,所有封裝的型別是無法通過放射獲取到的,像使用者自定義的型別,那用到了反射的框架如Spring現在該如何掃描型別呢?為了解決這個問題,平臺引入了一個概念叫開放模組open modules.要讓整個模組都open,只需要在module關鍵字前面加上open關鍵字。例如:open module <module-name> {}。這樣模組內容還是封裝的,但是在執行時它可以用反射所訪問到。當然也可以只對模組中的某些包open,甚至可以讓某個包只能被某個模組訪問,例如:
    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包還沒有模組化。所以在此不具體介紹程式碼遷移。

《道德經》第一章:
道可道,非常道。名可名,非常名。無名天地之始。有名萬物之母。故常無慾以觀其妙。常有欲以觀其徼。此兩者同出而異名,同謂之玄。玄之又玄,眾妙之門。

譯文:“道”如果可以用言語來表述,那它就是常“道”(“道”是可以用言語來表述的,它並非一般的“道”);“名”如果可以用文辭去命名,那它就是常“名”(“名”也是可以說明的,它並非普通的“名”)。“無”可以用來表述天地渾沌未開之際的狀況;而“有”,則是宇宙萬物產生之本原的命名。因此,要常從“無”中去觀察領悟“道”的奧妙;要常從“有”中去觀察體會“道”的端倪。無與有這兩者,來源相同而名稱相異,都可以稱之為玄妙、深遠。它不是一般的玄妙、深奧,而是玄妙又玄妙、深遠又深遠,是宇宙天地萬物之奧妙的總門(從“有名”的奧妙到達無形的奧妙,“道”是洞悉一切奧妙變化的門徑)。