Jigsaw 專案:Java 模組系統新手引導
前言
隨著 2017 年 10 月 Java 9 的釋出,Java 能夠使用模組系統了,但是中文網際網路上的資料太少,許多關於 Java 模組系統的文章都只是介紹了模組系統的好處,或者給了一些毫無組織的程式碼片段,新手在第一次使用模組系統時往往不知道如何下手。
好在 OpenJDK 官方文件給出了一個很詳細的新手引導,即使是從沒使用過模組系統的人,按照新手引導也能完成自己的第一個 Java 模組。我在這裡只是將其翻譯成中文(英語水平有限,如有紕漏,歡迎指出),希望更多人能學會如何使用模組系統,加速 Java 類庫的模組化程序。
原文地址:Project Jigsaw: Module System Quick-Start Guide
Jigsaw 專案:模組系統新手引導
這篇文件給開始使用模組系統的開發者提供了一個簡單示例。
示例中的檔案路徑使用前斜槓(/),路徑分隔符是冒號(:)。使用微軟的 Windows 作業系統的開發者在路徑中應當使用後斜槓(\),路徑分隔符為分號(;)。
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 ...
更多資訊
from: https://zhuanlan.zhihu.com/p/41129220