Java SE 9 模組化示例

Java SE 9 模組化示例

作者:Grey

原文地址:Java SE 9 模組化示例

說明

Java SE 9引入了模組系統,模組就是程式碼和數據的封裝體。模組的程式碼被組織成多個包,每個包中包含Java類和介面;模組的數據則包括資源文件和其他靜態資訊。

module-info.java文件中,我們可以用新的關鍵詞module來聲明一個模組。

Java 9將JDK劃分為多個模組以支援各種配置。參考JEP 200: The Modular JDK

可以使用如下命令查詢所有的模組(註:JDK 版本大於等於9)

java --list-modules

模組描述符是在名為module-info.java的文件中定義的模組聲明的編譯版本。每個模組聲明都以關鍵字module開頭,後跟唯一的模組名稱和括在大括弧中的模組正文,格式如下

module modulename { 
}

模組聲明的主體可以是空的,也可以包含各種模組指令(比如:exports, module, open, opens, provides, requires, uses, with等)。

下面介紹各種指令的含義

require 指令指定此模組依賴於另一個模組,這種關係稱為模組依賴項。每個模組必須顯式聲明其依賴項。當模組A需要模組B時,模組A稱為讀取模組B,模組B由模組A讀取。要指定對另一個模組的依賴關係,請使用 require,格式如下

requires modulename;

require static 指令,用於指示在編譯時需要模組,但在運行時是可選的。這稱為可選依賴項。

requires transitive 表示模組的隱含可讀性,

如果這樣寫

module a {
    requires b;
}

表示

  • a模組強制要求存在b模組,這稱為可靠配置。

  • a模組可以讀入b模組的文件,這稱為可讀性。

  • a模組可以訪問b模組的程式碼,這稱為可訪問性。

但是如果寫成

module a {
    requires transitive b;
}

表示:

如果其他模組比如c依賴於a模組,a模組使用requires transitive引用b模組,那麼c模組即便沒有直接聲明依賴於b模組,它對b模組也是具有可讀性的,這個稱之為隱含可讀性。

即:a requires transitive b加上c requires a,則c對b也是可讀。如果去掉transitive,則c對b不可讀。

exports 指令指定模組的一個包,其public類型(及其嵌套的public類型和protected類型)應可供所有其他模組中的程式碼訪問。

exports…to 指令使您能夠在逗號分隔的列表中精確指定哪些模組或模組的程式碼可以訪問導出的包,這稱為限定導出。

uses 指令指定此模組使用的服務,使該模組成為服務使用者。服務是實現介面或擴展uses指令中指定的抽象類的類的對象。

provides…with 指令指定模組提供服務實現,使模組成為服務提供者。指令的 provide 部分指定列出的介面或抽象類,而 with 部分指令指定實現介面或擴展抽象類的服務提供程式類的名稱。

open , opens, 和 opens…to 指令用於了解包中的所有類型以及某個類型的所有成員(甚至是其私有成員),無論您是否允許此功能。在 JDK 9 之前,這一功能是通過反射來實現的。

opens 指令

opens package

允許僅運行時訪問包(package)。

opens…to 指令

opens package to comma-separated-list-of-modules

允許特定模組(module)僅運行時訪問包(package)。

open 指令

open module modulename {
   // module directives
} 

允許僅運行時訪問模組中的所有包。

環境準備

本地的 JDK 版本應該大於等於 JDK 9。

使用示例

本文的示例均參考於:Project Jigsaw: Module System Quick-Start Guide

註:以下示例均在Windows下進行,在Mac或者Linux下把\\換成/javac的版本一定要大於等於JDK 9

示例1

新建一個example-01文件夾,作為項目根目錄,在項目根目錄下創建一個src文件夾,在這個文件夾下,創建一個文件夾作為module,文件夾名稱為:com.greetings。在com.greetings文件夾下新建module-info.java文件夾,同時,在com.greetings文件夾中新建包,
即在com.greetings文件夾下新建com文件夾,在com文件夾下新建greetings文件夾。在greetings文件夾下新建Main.java

目錄結構如下

- example-01
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java

module-info.java文件中內容如下

module com.greetings { 

}

Main.java中內容如下

package com.greetings;

public class Main {
    public static void main(String[] args) {
        System.out.println("Greetings!");
    }
}

example-01目錄下新建mods文件夾,用於存放編譯後的目錄,在mods文件夾中新建一個com.greetings的目錄,

目錄結構變成了如下

- example-01
    - mods
        - com.greetings
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java

example-01目錄下執行如下編譯命令,

javac -d mods\\com.greetings src\\com.greetings\\module-info.java src\\com.greetings\\com\\greetings\\Main.java

編譯完畢後,項目目錄如下

- example-01
    - mods
        - com.greetings
            - module-info.class
            - com
                - greetings
                    - Main.class
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java

接下來運行java命令進行執行

java --module-path mods -m com.greetings/com.greetings.Main

控制台輸出結果

Greetings!

--module-path是模組路徑,其值是一個或多個包含模組的目錄。-m選項指定的是主模組,斜線後的值是模組中主類的類全名。

示例2

如上例,我們準備好文件夾目錄和相關文件

- example-02
    - mods
        - com.greetings
        - org.astro
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java
        - org.astro
            - module-info.java
            - org
                - astro
                    - World.java

其中src\\org.astro\\module-info.java內容如下

module org.astro {
    exports org.astro;
}

src\\org.astro\\org\\astro\\World.java內容如下

package org.astro;
public class World {
    public static String name() {
        return "world";
    }    
}

src\\com.greetings\\module-info.java

module com.greetings {
    requires org.astro;
}

`src\com.greetings\com\greetings\Main.java

package com.greetings;
import org.astro.World;
public class Main {
    public static void main(String[] args) {
        System.out.format("Greetings %s!%n", World.name());
    }
}

然後,在example-02下分別對org.astrocom.greetings兩個模組進行編譯。

javac -d mods\\org.astro src\\org.astro\\module-info.java src\\org.astro\\org\\astro\\World.java
javac --module-path mods -d mods\\com.greetings src\\com.greetings\\module-info.java src\\com.greetings\\com\\greetings\\Main.java

編譯完成後,項目目錄如下

- example-02
    - mods
        - com.greetings
            - module-info.class
            - com
                - greetings
                    - Main.class
        - org.astro
            - module-info.class
            - org
                - astro
                    - World.class
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java
        - org.astro
            - module-info.java
            - org
                - astro
                    - World.java

然後運行

java --module-path mods -m com.greetings/com.greetings.Main

控制台列印

Greetings world!

示例3

本示例演示如何打包,新建example-03文件夾,把示例2的所有程式碼和目錄都原封不動拷貝進去。

- example-03
    - mods
        - com.greetings
        - org.astro
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java
        - org.astro
            - module-info.java
            - org
                - astro
                    - World.java

example-03文件夾中新建一個mlib的文件夾,用於存放打包後的文件。

- example-03
    - mlib
    - mods
        - com.greetings
        - org.astro
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java
        - org.astro
            - module-info.java
            - org
                - astro
                    - World.java

執行打包命令,根據依賴關係,應該先打org.astro的包。

jar --create --file=mlib\\[email protected] --module-version=1.0 -C mods\\org.astro .

然後再打com.greetings的包

jar --create --file=mlib\\com.greetings.jar --main-class=com.greetings.Main -C mods\\com.greetings .

打包完成後,目錄結構如下

- example-03
    - mlib
        - com.greetings.jar
        - [email protected]
    - mods
        - com.greetings
        - org.astro
    - src
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java
        - org.astro
            - module-info.java
            - org
                - astro
                    - World.java

接下來執行jar

java -p mlib -m com.greetings

控制台列印

Greetings world!

jar工具有許多新的選項(見jar -help),其中之一是列印作為模組化JAR打包的模組的模組聲明。

jar --describe-module --file=mlib/[email protected]

控制台輸出

[email protected] jar:file:///C:/workspace/hello-module/example-03/mlib/[email protected]!/module-info.class
exports org.astro
requires java.base mandated

示例4

服務允許服務消費者模組和服務提供者模組之間進行鬆散耦合。
這個例子有一個服務消費者模組和一個服務提供者模組。

模組com.socket輸出了一個網路套接字的API。該API在包com.socket中,所以這個包被導出了。該API是可插拔的,允許替代的實現。服務類型是同一模組中的com.socket.spi.NetworkSocketProvider類,因此包com.socket.spi也是導出的。
模組org.fastsocket是一個服務提供者模組。它提供了com.socket.spi.NetworkSocketProvider的一個實現。它沒有輸出任何包。

創建example-04文件夾和相關目錄

- example-04
    - mods
        - com.socket
        - com.greetings
        - org.fastsocket
    - src
        - com.socket
            - module-info.java
            - com
                - socket
                    - spi
                        - NetworkSocketProvider.java
                    - NetworkSocket.java
        - com.greetings
            - module-info.java
            - com
                - greetings
                    - Main.java
        - org.fastsocket
            - module-info.java
            - org
                - fastsocket
                    - FastNetworkSocketProvider
                    - FastNetworkSocket.java         

其中src\\com.socket\\module-info.java

    module com.socket {
        exports com.socket;
        exports com.socket.spi;
        uses com.socket.spi.NetworkSocketProvider;
    }

src\\com.socket\\com\\socket\\NetworkSocket.java程式碼如下

    package com.socket;

    import java.io.Closeable;
    import java.util.Iterator;
    import java.util.ServiceLoader;

    import com.socket.spi.NetworkSocketProvider;

    public abstract class NetworkSocket implements Closeable {
        protected NetworkSocket() { }

        public static NetworkSocket open() {
            ServiceLoader<NetworkSocketProvider> sl
                = ServiceLoader.load(NetworkSocketProvider.class);
            Iterator<NetworkSocketProvider> iter = sl.iterator();
            if (!iter.hasNext())
                throw new RuntimeException("No service providers found!");
            NetworkSocketProvider provider = iter.next();
            return provider.openNetworkSocket();
        }
    }

src\\com.socket\\com\\socket\\spi\\NetworkSocketProvider.java程式碼如下

    package com.socket.spi;

    import com.socket.NetworkSocket;

    public abstract class NetworkSocketProvider {
        protected NetworkSocketProvider() { }

        public abstract NetworkSocket openNetworkSocket();
    }

src\\org.fastsocket\\module-info.java

    module org.fastsocket {
        requires com.socket;
        provides com.socket.spi.NetworkSocketProvider
            with org.fastsocket.FastNetworkSocketProvider;
    }

src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocketProvider.java程式碼如下

    package org.fastsocket;

    import com.socket.NetworkSocket;
    import com.socket.spi.NetworkSocketProvider;

    public class FastNetworkSocketProvider extends NetworkSocketProvider {
        public FastNetworkSocketProvider() { }

        @Override
        public NetworkSocket openNetworkSocket() {
            return new FastNetworkSocket();
        }
    }

src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocket.java程式碼如下

    package org.fastsocket;

    import com.socket.NetworkSocket;

    class FastNetworkSocket extends NetworkSocket {
        FastNetworkSocket() { }
        public void close() { }
    }

src/com.greetings/module-info.java程式碼如下

    module com.greetings {
        requires com.socket;
    }

src\\com.greetings/com\\greetings\\Main.java程式碼如下

    package com.greetings;

    import com.socket.NetworkSocket;

    public class Main {
        public static void main(String[] args) {
            NetworkSocket s = NetworkSocket.open();
            System.out.println(s.getClass());
        }
    }

接下來是編譯,根據依賴順序

先編譯com.socket

javac -d mods --module-source-path src src\\com.socket\\module-info.java src\\com.socket\\com\\socket\\NetworkSocket.java src\\com.socket\\com\\socket\\spi\\NetworkSocketProvider.java

再編譯org.fastsocket

javac -d mods --module-source-path src src\\org.fastsocket\\module-info.java src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocket.java src\\org.fastsocket\\org\\fastsocket\\FastNetworkSocketProvider.java  

最後編譯com.greetings

javac -d mods --module-source-path src src\\com.greetings\\module-info.java src\\com.greetings\\com\\greetings\\Main.java

最後執行

java -p mods -m com.greetings/com.greetings.Main

控制台列印出結果

class org.fastsocket.FastNetworkSocket

以上輸出確認了服務提供者已經被定位,並且它被用作NetworkSocket的工廠。

示例5

jlink是鏈接器工具,可以用來鏈接一組模組,以及它們的橫向依賴關係,以創建一個自定義的模組化運行時鏡像(見JEP 220)。
該工具目前要求模組路徑上的模組以模組化的JARJMOD格式打包。JDK構建以JMOD格式打包標準模組和JDK專用模組。

下面的例子創建了一個運行時鏡像,其中包含com.greetings模組和它的橫向依賴:

jlink --module-path $JAVA_HOME/jmods:mlib --add-modules com.greetings --output greetingsapp

--module-path的值是一個包含打包模組的目錄PATH。在Microsoft Windows上,將路徑分隔符’:’替換為’;’。
$JAVA_HOME/jmods是包含java.base.jmod及其他標準和 JDK模組的目錄。

模組路徑上的mlib目錄包含com.greetings模組的工件。

jlink工具支援許多高級選項來訂製生成的影像,更多選項見jlink --help

示例6

–patch-module
從Doug Lea的CVS中籤出java.util.concurrent類的開發者將習慣於用-Xbootclasspath/p來編譯源文件和部署這些類。
-Xbootclasspath/p已經被刪除,它的模組替代品是選項--patch-module,用於覆蓋模組中的類。它也可以被用來增加模組的內容。javac也支援--patch-module選項,可以像模組的一部分一樣編譯程式碼。
下面是一個編譯新版本的java.util.concurrent.ConcurrentHashMap並在運行時使用它的例子:

javac --patch-module java.base=src -d mypatches/java.base \
        src/java.base/java/util/concurrent/ConcurrentHashMap.java
java --patch-module java.base=mypatches/java.base ...

程式碼

hello-module

參考資料

Project Jigsaw: Module System Quick-Start Guide

Understanding Java 9 Modules

Module System in JDK 9

Java 9 Module Example

Java 9 模組系統

Java 9 Modules – Developing Java 9 Modules with Apache Maven

Java 9 module example using maven