天天看點

Jigsaw 項目:Java 子產品系統新手引導

前言

随着 2017 年 10 月 Java 9 的釋出,Java 能夠使用子產品系統了,但是中文網際網路上的資料太少,許多關于 Java 子產品系統的文章都隻是介紹了子產品系統的好處,或者給了一些毫無組織的代碼片段,新手在第一次使用子產品系統時往往不知道如何下手。

好在 OpenJDK 官方文檔給出了一個很詳細的新手引導,即使是從沒使用過子產品系統的人,按照新手引導也能完成自己的第一個 Java 子產品。我在這裡隻是将其翻譯成中文(英語水準有限,如有纰漏,歡迎指出),希望更多人能學會如何使用子產品系統,加速 Java 類庫的子產品化程序。

Jigsaw 項目:子產品系統新手引導

這篇文檔給開始使用子產品系統的開發者提供了一個簡單示例。

示例中的檔案路徑使用前斜杠(/),路徑分隔符是冒号(:)。使用微軟的 Windows 作業系統的開發者在路徑中應當使用後斜杠(\),路徑分隔符為分号(;)。

  • ​​Greetings​​
  • ​​Greetings world​​
  • ​​多子產品編譯​​
  • ​​打包​​
  • ​​缺少導入或缺少導出​​
  • ​​服務​​
  • ​​連結​​
  • ​​--patch-module​​
  • ​​更多資訊​​

Greetings

第一個示例是一個名叫​

​com.greetings​

​的子產品,它隻是簡單的列印一句“Greetings”。這個子產品由兩個源檔案組成:子產品聲明檔案(module-info.java)和主類。

為了友善,子產品的源碼放在一個和子產品名相同的目錄中。

src/com.greetings/com/greetings/Main.java
src/com.greetings/module-info.java

$ cat src/com.greetings/module-info.java

$ cat src/com.greetings/com/greetings/Main.java
package com.greetings;
public class Main {
    public static void main(String[] args) {
        System.out.println("Greetings!");
    }
}
      

源碼被下面的指令編譯到目标目錄​

​mods/com.greetings​

​中去:

$ mkdir -p mods/com.greetings

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

現在我們用下面的指令來運作示例:

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

​--module-path​

​是子產品路徑,它的值是一個或多個包含子產品的目錄。​

​-m​

​選項指定主子產品,分隔符後面的值是主子產品中包含 main 方法的類的類名。

Greetings world

第二個示例更新了​

​org.astro​

​子產品的子產品聲明檔案來聲明依賴。子產品​

​org.astro​

​導出了 API 包​

​org.astro​

​。

src/org.astro/module-info.java
src/org.astro/org/astro/World.java
src/com.greetings/com/greetings/Main.java
src/com.greetings/module-info.java

$ cat src/org.astro/module-info.java
module org.astro {
    exports org.astro;
}

$ cat src/org.astro/org/astro/World.java
package org.astro;
public class World {
    public static String name() {
        return "world";
    }
}

$ cat src/com.greetings/module-info.java
module com.greetings {
    requires org.astro;
}

$ cat 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());
    }
}
      

子產品被依次編譯。編譯​

​com.greetings​

​子產品的​

​javac​

​指令指定了一個子產品路徑,是以對子產品​

​org.astro​

​的引用、以及它所導出的包中的類型都可以被找到。

$ mkdir -p mods/org.astro mods/com.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
      

這個示例的運作方式和第一個例子完全一樣:

$ java --module-path mods -m com.greetings/com.greetings.Main
Greetings world!
      

多子產品編譯

在前面的示例中,子產品​

​com.greetings​

​和子產品​

​org.astro​

​是分别編譯的。其實在一個​

​javac​

​指令中編譯多個子產品也是可以的:

$ mkdir mods

$ javac -d mods --module-source-path src $(find src -name "*.java")

$ find mods -type f
mods/com.greetings/com/greetings/Main.class
mods/com.greetings/module-info.class
mods/org.astro/module-info.class
mods/org.astro/org/astro/World.class
      

打包

目前為止,示例中被編譯的子產品散落在檔案系統中。為了更友善的傳輸與部署,通常會将子產品打包成一個modular JAR(子產品化的 JAR 包)。一個 modular JAR 相當于一個包含了一個 module-info.class 在它的頂層目錄的普通 JAR 包。下面的例子在目錄 mlib 中建立了一個​

[email protected]

​和​

​com.greetings.jar​

$ mkdir mlib

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

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

$ ls mlib
com.greetings.jar   [email protected]
      

在這個例子中,子產品​

​org.astro​

​被打包時辨別了它的版本号是 1.0 。子產品​

​com.greetings​

​被打包時辨別了它的主類是​

​com.greetings.Main​

​。我們現在可以不需要指定主類來執行子產品​

​com.greetings​

​了:

$ java -p mlib -m com.greetings
Greetings world!
      

通過使用​

​-p​

​來代替​

​--module-path​

​,指令可以被縮短。

jar 工具有許多新的選項(通過​

​jar -help​

​檢視),其中一個就是列印一個 modular JAR 的子產品聲明。

$ jar --describe-module --file=mlib/[email protected]
[email protected] jar:file:///d/mlib/[email protected]/!module-info.class
exports org.astro
requires java.base mandated
      

缺少導入或缺少導出

現在我們來看看對于前面的例子,如果在子產品​

​com.greetings​

​的子產品聲明中,我們不小心漏寫了引用項(requires)将會發生什麼。

$ cat src/com.greetings/module-info.java
module com.greetings {
    // requires org.astro;
}

$ javac --module-path mods -d mods/com.greetings \
    src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
src/com.greetings/com/greetings/Main.java:2: error: package org.astro is not visible
    import org.astro.World;
              ^
  (package org.astro is declared in module org.astro, but module com.greetings does not read it)
1 error
      

我們現在修複了這個子產品聲明,但是卻引入了另一個錯誤,這次我們漏寫了子產品​

​org.astro​

​的子產品聲明中的導出項(exports):

$ cat src/com.greetings/module-info.java
module com.greetings {
    requires org.astro;
}
$ cat src/org.astro/module-info.java
module org.astro {
    // exports org.astro;
}

$ javac --module-path mods -d mods/com.greetings \
    src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
$ javac --module-path mods -d mods/com.greetings \
    src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
src/com.greetings/com/greetings/Main.java:2: error: package org.astro is not visible
    import org.astro.World;
              ^
  (package org.astro is declared in module org.astro, which does not export it)
1 error
      

服務

服務能夠讓服務消費者子產品和服務提供者子產品解耦。

這個例子有一個服務消費者子產品和一個服務提供者子產品:

- 子產品​

​com.socket​

​導出了網絡套接字的 API 。這個 API 在包​

​com.socket​

​中是以這個包被導出。這個 API 是可拔插的,允許不同的實作。服務類型是相同子產品中的​

​com.socket.spi.NetworkSocketProvider​

​類型,是以包​

​com.socket.spi​

​也被導出。

​org.fastsocket​

​是一個服務提供者子產品。它提供了一個對​

​com.socket.spi.NetworkSocketProvider​

​的實作。它不對導出任何包。

下面的是子產品​

​com.socket​

​的源碼:

$ cat src/com.socket/module-info.java
module com.socket {
    exports com.socket;
    exports com.socket.spi;
    uses com.socket.spi.NetworkSocketProvider;
}

$ cat 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();
    }
}


$ cat 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();
}
      

​org.fastsocket​

$ cat src/org.fastsocket/module-info.java
module org.fastsocket {
    requires com.socket;
    provides com.socket.spi.NetworkSocketProvider
        with org.fastsocket.FastNetworkSocketProvider;
}

$ cat 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();
    }
}

$ cat src/org.fastsocket/org/fastsocket/FastNetworkSocket.java
package org.fastsocket;

import com.socket.NetworkSocket;

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

為了簡潔,我們同時編譯兩個子產品。在實踐中服務消費者子產品和服務提供者子產品幾乎總是分别編譯的。

$ mkdir mods
$ javac -d mods --module-source-path src $(find src -name "*.java")
      

最後我們修改我們的​

​com.greetings​

​子產品來使用 API 。

$ cat src/com.greetings/module-info.java
module com.greetings {
    requires com.socket;
}

$ cat 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());
    }
}


$ javac -d mods/com.greetings/ -p mods $(find src/com.greetings/ -name "*.java")
      

最後我們來運作它:

$ java -p mods -m com.greetings/com.greetings.Main
class org.fastsocket.FastNetworkSocket
      

輸出結果證明服務提供者是存在的,并且它被​

​NetworkSocket​

​作為工廠使用。

連結

jlink 是一個連結工具,可以沿着依賴鍊來連結一組子產品,建立一個使用者子產品運作時鏡像(見 ​​JEP 220​​)。

該工具目前要求子產品路徑中的子產品都是被用 modular JAR 或者 JMOD 格式打包的。JDK 建構用 JMOD 格式打包标準的、和 JDK 指定的子產品。

下面的例子建立了一個包含​

​com.greetings​

​子產品以及其傳遞依賴的運作時鏡像:

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

​--module-path​

​的值是包含了要打包的子產品的 路徑。在微軟的 Windows 作業系統中要将路徑分隔符 ':' 替換為 ';' 。

​$JAVA_HOME/jmods​

​是包含了​

​java.base.jmod​

​和其他标準 JDK 子產品的目錄。

子產品路徑中的​

​mlib​

​目錄包含了子產品​

​com.greetings​

​的部件(artifact)。

jlink 工具支援許多進階的選項來自定義生成了鏡像,用​

​jlink --help​

​檢視更多選項。

--patch-module

從 Doug Lea 的 CVS 中檢視​

​java.util.concurrent​

​包中的 class 檔案的開發者将會習慣使用​

​-Xbootclasspath/p​

​編譯源檔案和部署這些 class 檔案。

​-Xbootclasspath/p​

​已經被移除,在一個子產品中,用來覆寫 class 檔案的子產品替換選項是​

​--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 ...