java9並沒有在語言層面做出很多改變,而是致力於一些新特性,如模塊化,其核心就是解決歷史遺留問題,為以后的jar包森林理清道路。模塊化是一個很大的命題,就不講那么細致了,關於java9的特性也有很多書籍可供參考,如
《Java 9 Revealed: For Early Adoption and Migration》,《Java 9 Modularity》,還有很多很多(點擊查看)。
模塊化是一個很大的特性,也是一個很大的話題。模塊化作為一個未來的的趨勢,我們可能會關心項目的遷移,對接OSGI或者微服務等等,本文只是簡單闡述下模塊化解決的基本問題和模塊化的一些用法。
模塊化
兩個問題,臃腫的jar包和混亂的jar包依賴關系。如果我們沒被現在的jar包系統搞瘋,首先應該感謝IDE和maven,gradle之類的版本控制軟件。你可能見過這個大家伙,60多m的jar包,運行一個Hello World
程序,也需要這么一個龐然大物支撐。
再或者,我們依賴一個大的工具包,但我們可能只需要其中幾個類的功能。之前的jar包機制是很僵硬的,我們需要更靈活更輕便條理更清晰的環境,從化繁為簡的角度來講,我們需要模塊化。
不知道你經歷過jar hell嗎,可以參考下圖:
jar包之間的依賴可謂錯綜復雜,jar包之間交叉,纏繞,沖突,旋轉,跳躍,我覺得沒有人會想理清項目中依賴的這一個jar包與其他Jar包之間千絲萬縷的聯系。不看依賴,便是晴天,但是我們有時候真的需要理清楚,因為我們在部署的時候可能會產生沖突,會報各種異常。我們理清楚了,我們對這個jar包理解更深了,我們變強了,我們禿了。我們需要更清晰的圖像來描述依賴關系,我們不能任由jar纏繞旋轉,我們應該讓他們更守規矩,讓依賴圖像清楚並且有層次,我們需要模塊化。
其實我們不需要理解模塊化的字面意義,我們需要知道我們面臨的困難和要解決的問題,jdk9發布的解決這些問題的新特性的名稱叫做模塊化。
漫長的模塊化
在使用模塊化之前,我需要讓你明白一些事情,模塊化不是一蹴而就,java平台做了表率,從內部進行了模塊化改造。我們先來看一下傳統的jdk7的API:
很熟悉的api形式。
再來看一看java9的api:
點進java.activation
我們還可以看到模塊的圖像,Java9不僅為我們提供了模塊化的手段,整個java9平台自身,也已經被模塊化改造了。整個java生態徹底改造成模塊化是需要時間的,從jdk7時提出模塊化概念,到整個模塊化版本發布,經歷了很長時間。把原來的第三方jar包徹底改造成模塊化jar包會花費很多功夫,我們能夠使用jdk的模塊化包,不代表我們能夠使用整個Java生態中所有的模塊化包,這需要開發者對自己的jar包進行調整。
- 模塊化為我們梳理出了邏輯清晰的jar包系統。
- 模塊化需要額外的操作,處理相同的代碼,我們需要理清代碼之間的關系,對內容模塊化,但IDE提供了一些便捷的功能。
- 擁抱徹底的模塊化生態需要時間,很多重量級的框架和jar包適應模塊化是需要我們等待的,但是有自動化模塊可以幫助我們構建模塊化項目。
簡單入門模塊化
在$JAVA_HOME/bin
中的Java工具也為模塊化提供了很多新功能,就不一一詳細介紹了,感興趣的話可以參考Oracle的介紹。下面的例子主要是參考openJDK模塊化的快速入門和GitHub上的一個項目,很好的例子,我也就不做太多更改了,會簡要敘述下例子中的新事物。
首先要下載JDK9,可以去官網下載,我是用Linux平台演示的代碼。
先來看第一個例子,輸出Greetings!
先來看一看整個項目的目錄結構:
src
└── src/com.greetings
├── src/com.greetings/com
│ └── src/com.greetings/com/greetings
│ └── src/com.greetings/com/greetings/Main.java
└── src/com.greetings/module-info.java
整個項目包含兩個文件,一個是module-info.java
模塊文件,還有一個是Main.java
主類文件。讓我們看一看這兩個文件的內容。
$ cat src/com.greetings/module-info.java
module com.greetings { }
模塊文件中沒有內容,因為這個Main.java
中沒有依賴別的模塊的內容,也沒有打算向外界暴露Main.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!");
}
}
然后,我們把這兩個文件編譯運行。
javac -d mods/com.greetings \
src/com.greetings/module-info.java \
src/com.greetings/com/greetings/Main.java
javac -d 指定放置生成的類文件的位置
java --module-path mods \
-m com.greetings/com.greetings.Main
#或者
java --module-path mods \
--module com.greetings/com.greetings.Main
就可以運行了,我們會發現屏幕上打印出了Greetings!
其中 --module-path <模塊路徑>...用 : 分隔的目錄列表, 每個目錄都是一個包含模塊的目錄。
-m或--module是一樣的,用於指定要解析的初始化模塊名稱,如果不指定模塊名稱,可以指定要執行的主類名稱
再來看第二個例子,Greetings world!
我們先來看一下目錄結構:
src
├── src/com.greetings
│ ├── src/com.greetings/com
│ │ └── src/com.greetings/com/greetings
│ │ └── src/com.greetings/com/greetings/Main.java
│ └── src/com.greetings/module-info.java
└── src/org.astro
├── src/org.astro/module-info.java
└── src/org.astro/org
└── src/org.astro/org/astro
└── src/org.astro/org/astro/World.java
再來看看代碼內容:
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());
}
}
cat src/com.greetings/module-info.java
module com.greetings {
requires org.astro;
}
cat src/org.astro/org/astro/World.java
package org.astro;
public class World {
public static String name() {
return "world";
}
}
cat src/org.astro/module-info.java
module org.astro {
exports org.astro;
}
Main.java
類中打印了Greetings!+World.name()
,com.greetings
模塊文件聲明了requires org.astro
,意思就是我這個模塊需要依賴org.astro
模塊;World
類中有一個靜態方法,返回了world
字符串,com.greetings
模塊依賴於它,而且他在自己的模塊文件中做出了聲明exports org.astro
,將org.astro
模塊暴露出去,可供其他模塊依賴。
#我們先編譯com.greetings模塊試試
javac -d mods/com.greetings \
> > src/com.greetings/module-info.java src/com.greetings/com/greetings/Main.java
src/com.greetings/module-info.java:2: 錯誤: 找不到模塊: org.astro
requires org.astro;
^
1 個錯誤
那再先編譯一下,org.astro模塊
javac -d mods/org.astro \
> src/org.astro/module-info.java src/org.astro/org/astro/World.java
然后編譯一下com.greeetings
javac --module-path mods -d mods/com.greetings src/com.greetings/com/greetings/Main.java src/com.greetings/module-info.java
--module-path(或-p) 用來指定應用的模塊的位置。
運行一下
java --module-path mods -m com.greetings/com.greetings.Main
//Greetings world!
這個例子中要注意exports
和requires
的用法,exports
對外界暴露內容,requires
引入外界功能,如果沒有引入,或者引入不存在的module編譯無法通過。
#如果沒有聲明requires
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
#如果沒有聲明exports
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
多模塊的編譯
可以一次編譯多個模塊
我們先找出所有.java文件
echo $(find src -name "*.java")
//src/org.astro/org/astro/World.java src/org.astro/module-info.java src/com.greetings/com/greetings/Main.java src/com.greetings/module-info.java
一口氣將所有.java文件編譯到mods文件夾中
javac -d mods --module-source-path src $(find ./src -name "*.java")
看一下目錄結構
mods
├── mods/com.greetings
│ ├── mods/com.greetings/com
│ │ └── mods/com.greetings/com/greetings
│ │ └── mods/com.greetings/com/greetings/Main.class
│ └── mods/com.greetings/module-info.class
└── mods/org.astro
├── mods/org.astro/module-info.class
└── mods/org.astro/org
└── mods/org.astro/org/astro
└── mods/org.astro/org/astro/World.class
效果是一樣的,再運行一下
java --module-path mods -m com.greetings/com.greetings.Main
//Greetings world!
- 打包
我們可以把模塊打成jar包,這樣會更方便,模塊jar包里頂級目錄下會有一個module-info.calss
文件。
我們把已經編譯好的mods文件夾中的模塊文件打包成jar包
注意:最后的句號不能漏掉
jar -c -f mlib/org.astro@1.0.jar \
--module-version=1.0 -C mods/org.astro .
//或者
jar --create --file=mlib/org.astro@1.0.jar \
--module-version=1.0 -C mods/org.astro .
兩個都可以的,--create縮寫就是-c,意思是我們創建jar包;--file=縮寫就是-f,指定文件位置和名稱,--module-version是指定版本,創建模塊化 jar 或更新非模塊化 jar 時的模塊版本;-C 就是更改為指定的目錄並包含以下文件。關於jar命令還有很多操作,可以通過jar --help查閱,或者去官網查閱。
$ jar --create --file=mlib/com.greetings.jar \
--main-class=com.greetings.Main -C mods/com.greetings .
產看一下生成的jar包
$ ls mlib
com.greetings.jar org.astro@1.0.jar
來執行一下,因為我們打包com.greetings模塊時,指定了主類,所以運行時可以直接調用
java -p mlib -m com.greetings
//Greetings world!
Service
如何使用模塊系統提供服務呢,讓我們來看看。
首先查看一下工程的結構:
src
├── src/com.greetings
│ ├── src/com.greetings/com
│ │ └── src/com.greetings/com/greetings
│ │ └── src/com.greetings/com/greetings/Main.java
│ └── src/com.greetings/module-info.java
├── src/com.socket
│ ├── src/com.socket/com
│ │ └── src/com.socket/com/socket
│ │ ├── src/com.socket/com/socket/NetworkSocket.java
│ │ └── src/com.socket/com/socket/spi
│ │ └── src/com.socket/com/socket/spi/NetworkSocketProvider.java
│ └── src/com.socket/module-info.java
└── src/org.fastsocket
├── src/org.fastsocket/module-info.java
└── src/org.fastsocket/org
└── src/org.fastsocket/org/fastsocket
├── src/org.fastsocket/org/fastsocket/FastNetworkSocket.java
└── src/org.fastsocket/org/fastsocket/FastNetworkSocketProvider.java
有點多,我們先理一理其中的關系
com.scoket中還有個NetworkSocketProvider.java
,其實就是用來創建socket的一個工廠。我們再來看看他們的代碼:
先來看看org.socket模塊:
package com.socket;
import com.socket.spi.NetworkSocketProvider;
import java.io.Closeable;
import java.util.Iterator;
import java.util.ServiceLoader;
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();
}
}
其中有一個open()
方法,其實就是使用provider
生成一個NetworkSocket
對象並返回。
再來看看NetworkSocketProvider:
package com.socket.spi;
import com.socket.NetworkSocket;
public abstract class NetworkSocketProvider {
protected NetworkSocketProvider() {}
public abstract NetworkSocket openNetworkSocket();
}
一個抽象類,openNetworkSocket()
抽象方法由子類實現。
module-info.java
module com.socket {
exports com.socket;
exports com.socket.spi;
uses com.socket.spi.NetworkSocketProvider;
}
我想上面兩個exports你應該能夠理解其意義,但是uses是什么意思呢。為了使用服務,它的提供者需要被發現和加載。java.util.ServiceLoader類來發現和加載服務。發現和加載服務提供者的模塊必須在其聲明中包含一個uses語句,該語句語法如下:uses <service-interface>
;<service-interface>
是服務接口的名稱,它是Java接口名稱,類名稱或注解類型名稱。如果一個模塊使用ServiceLoader<S>
類加載名為S的服務接口的服務提供者的實例,則模塊聲明必須包含以uses S
。總而言之,就是你如果是個服務接口,你需要服務提供的類實現你的接口,你就需要聲明uses
。
那我們在看一看服務提供者類,org.fastsocket
:
//繼承了NetWorkSocket類
package org.fastsocket;
import com.socket.NetworkSocket;
public class FastNetworkSocket extends NetworkSocket {
FastNetworkSocket() {}
public void close() {}
}
再來看看FastNetworkSocketProvider
類,它實現了NetworkSocketProvider
抽象類,重寫了抽象方法,用來返回一個NetworkSocket。
package org.fastsocket;
import com.socket.NetworkSocket;
import com.socket.spi.NetworkSocketProvider;
public class FastNetworkSocketProvider extends com.socket.spi.NetworkSocketProvider {
public FastNetworkSocketProvider() {}
@Override
public NetworkSocket openNetworkSocket() {
return new FastNetworkSocket();
}
}
再來看看module-info.java
module org.fastsocket {
requires com.socket;
provides com.socket.spi.NetworkSocketProvider
with org.fastsocket.FastNetworkSocketProvider;
}
其中有個provides
和with
,意思就是為NetworkSocketProvider
提供一個實現類org.fastsocket.FastNetworkSocketProvider
。
再來快速看一下客戶端代碼,我們運行一下整個服務系統。
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());
}
}
cat src/com.greetings/module-info.java
module com.greetings {
requires com.socket;
}
客戶端代碼很簡單,客戶端模塊依賴了com.socket
,Main.java
打印了提供服務的類名。
我們先試着編譯服務接口代碼和客戶端代碼:
javac -d mods/com.socket $(find src/com.socket -name "*.java")
javac --module-path mods -d mods/com.greetings $(find src/com.greetings -name "*.java")
編譯時並沒有報錯。是的,服務接口中雖然聲明了uses
關鍵字,意味着這個模塊需要實現類,但是在編譯時他不會尋找需要的實現類模塊。
運行一下看看。
java -p mods -m com.greetings/com.greetings.Main
Exception in thread "main" java.lang.RuntimeException: No service providers found!
at com.socket/com.socket.NetworkSocket.open(NetworkSocket.java:16)
at com.greetings/com.greetings.Main.main(Main.java:7)
報了一個運行時異常,意味着對於服務的具體實現是在運行是尋找的。
我們把實現類編譯一下,然后運行:
javac --module-path mods -d mods/org.fastsocket $(find src/org.fastsocket -name "*.java")
java -p mods -m com.greetings/com.greetings.Main
class org.fastsocket.FastNetworkSocket
成功打印出了類名,是不是有點像OSGI的熱插拔。通過模塊系統,我們實現了對服務接口和服務提供者的解耦,我們確保服務提供者在調用服務之前能夠編譯到模塊中即可。
模塊補丁
什么是模塊補丁呢,假如我們的項目依賴了一個A模塊,但是覺得需要自定義A模塊中某些類的功能,那可以修改這個類,然后把修改后的類打到依賴的A模塊中。這樣,我們的項目就可以使用擁有自定義類的模塊了。下面以ConcurrentHashMap
為例,ConcurrentHashMap
是java.base
模塊中的一個類,我們將自定義的ConcurrentHashMap
類替換java.base
中ConcurrentHashMap
。
先來看一看整個工程的結構:
├── src/com.greetings
│ ├── src/com.greetings/com
│ │ └── src/com.greetings/com/greetings
│ │ └── src/com.greetings/com/greetings/Main.java
│ └── src/com.greetings/module-info.java
└── src/java.base
└── src/java.base/java
└── src/java.base/java/util
└── src/java.base/java/util/concurrent
└── src/java.base/java/util/concurrent/ConcurrentHashMap.java
其中有兩個模塊,com.greeting
模塊中依賴了java.base
中的ConcurrentHashMap
,java.base
模塊里包含了我們重寫的ConcurrentHashMap
類,而且注意一點,這個模塊中沒有module-info.java
描述文件。再來看看模塊的內容:
module com.greetings {
}
package com.greetings;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class Main {
public static void main(String[] args) {
Map<String, Integer> myMap = new ConcurrentHashMap<>();
myMap.put("one", 1);
myMap.put("two", 2);
myMap.put("three", 3);
System.out.println("Hello " + myMap);
System.out.println("Is Duke home? " + myMap.containsKey("Duke"));
}
}
雖然module-info.java
中沒有聲明依賴java.base
,但是其實所有的模塊都是隱式依賴java.base
模塊的,java.base
是所有模塊的基本模塊,他暴露了java.lang
和java.util
等等基礎包。Main.java
中的內容也很簡單,放入ConcurrentHashMap
中幾個元素,然后打印。
再來看看我們重寫的ConcurrentHashMap
的內容,可以只看put()
方法,containsKey()
方法和toString()
方法:
package java.util.concurrent;
import java.io.Serializable;
import java.util.*;
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable {
private final Map<K, V> innerMap;
public ConcurrentHashMap() {
this.innerMap = new HashMap<>();
}
public ConcurrentHashMap(int initialCapacity) {
this.innerMap = new HashMap<>(initialCapacity);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
this.innerMap = new HashMap<>(initialCapacity, loadFactor);
}
public Set<Map.Entry<K, V>> entrySet() {
return innerMap.entrySet();
}
@Override
public boolean replace(K key, V oldValue, V newValue) {
return innerMap.replace(key, oldValue, newValue);
}
@Override
public V replace(K key, V value) {
return innerMap.replace(key, value);
}
@Override
public V remove(Object o) {
return innerMap.remove(o);
}
@Override
public boolean remove(Object key, Object value) {
return innerMap.remove(key, value);
}
@Override
public V putIfAbsent(K key, V value) {
return innerMap.putIfAbsent(key, value);
}
@Override
public V put(K key, V value) {
return innerMap.put(key, value);
}
@Override
public V get(Object key) {
return innerMap.get(key);
}
@Override
public void putAll(Map<? extends K, ? extends V> map) {
innerMap.putAll(map);
}
@Override
public boolean containsKey(Object key) {
if (Objects.equals(key, "Duke")) {
return true;
}
return super.containsKey(key);
}
public String toString() {
return "patched ConcurrentHashMap " + super.toString() + " + Duke";
}
}
好了,這就是我們自定義的ConcurrentHashMap
類,我們先運行一下不打補丁的com.greetings
模塊。
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 \
--module com.greetings/com.greetings.Main
Hello {one=1, two=2, three=3}
Is Duke home? false
在沒有補丁的情況下,使用的是原來的ConcurrentHashMap
類。
試一試對java.base
打上補丁:
javac --patch-module java.base=src \
-d mypatches/java.base \
src/java.base/java/util/concurrent/ConcurrentHashMap.java
java --module-path mods \
--patch-module java.base=mypatches/java.base \
--module com.greetings/com.greetings.Main
Hello patched ConcurrentHashMap {one=1, two=2, three=3} + Duke
Is Duke home? true
成功了!我們在運行Main
方法時候,使用的是自定義的ConcurrentHashMap
類,來看看是如何做到的。首先在編譯的時候,我們使用了--patch-module
這個命令,查看一下javac的幫助文檔javac --help-extra
可以看到這個命令的解釋
--patch-module <模塊>=<文件>(:<文件>)*,使用 JAR文件或目錄中的類和資源覆蓋
所以呢,我們把java.base
中的ConcurrentHashMap
類給覆蓋了,在運行時,也有一個--patch-module
,其實和上面的解釋是一樣的,運行時使用mypatches/java.base
文件覆蓋java.base
文件,然后得到了想要的結果。
自動模塊化
當我們已經開始模塊化的工程,但是需要依賴一些非模塊化的jar包,這些庫的開發人員並沒有及時更新庫使其適應模塊化,他們缺少模塊化的描述符,所以我們需要自動模塊化--將非模塊化的jar包轉化成模塊化jar。以junit
為例,我們先來看看junit
jar包的描述信息。
jar -d --file=lib/junit-4.12.jar
找不到模塊描述符。已派生自動模塊。
junit@4.12 automatic
requires java.base mandated
contains junit.extensions
contains junit.framework
contains junit.runner
contains junit.textui
contains org.junit
...
-d等同於--describe-module
,用於輸出模塊描述符或自動模塊名稱,--file=指定文件名,我們可以看到他為我們派生出junit
的版本和描述符。但是不幸的是,他沒有告訴我們junit其實還依賴了Hamcrest
,我們通過jdeps工具來查看jar包的依賴。
jdeps -s lib/junit-4.12.jar
junit-4.12.jar -> java.base
junit-4.12.jar -> java.management
junit-4.12.jar -> 找不到
-s只是查看以來的概覽,有些依賴是不確定的,那我們看看丟失了哪些依賴:
jdeps lib/junit-4.12.jar | grep "找不到"
junit-4.12.jar -> 找不到
org.junit -> org.hamcrest 找不到
org.junit.experimental.results -> org.hamcrest 找不到
org.junit.internal -> org.hamcrest 找不到
org.junit.internal.matchers -> org.hamcrest 找不到
org.junit.matchers -> org.hamcrest 找不到
org.junit.matchers -> org.hamcrest.core 找不到
org.junit.rules -> org.hamcrest 找不到
找到了!那我們看看Hamcrest
需要哪些依賴:
jar -d -f lib/hamcrest-core-1.3.jar
找不到模塊描述符。已派生自動模塊。
hamcrest.core@1.3 automatic
requires java.base mandated
contains org.hamcrest
contains org.hamcrest.core
contains org.hamcrest.internal
jdeps -s lib/hamcrest-core-1.3.jar
hamcrest-core-1.3.jar -> java.base
Hamcrest
沒有缺失的依賴了,下面我們看看src文件的內容:
src
└── src/com.greetings
├── src/com.greetings/main
│ └── src/com.greetings/main/java
│ └── src/com.greetings/main/java/com
│ └── src/com.greetings/main/java/com/greetings
│ ├── src/com.greetings/main/java/com/greetings/Greet.java
│ └── src/com.greetings/main/java/com/greetings/Main.java
├── src/com.greetings/module-info.java
└── src/com.greetings/test
└── src/com.greetings/test/java
└── src/com.greetings/test/java/com
└── src/com.greetings/test/java/com/greetings
└── src/com.greetings/test/java/com/greetings/GreetTest.java
這是src的目錄結構,然后再看看四個文件的內容:
cat src/com.greetings/module-info.java
module com.greetings {
//1:聲明需要的模塊
//2:設置對自動化模塊可訪問
}
cat src/com.greetings/main/java/com/greetings/Greet.java
package com.greetings;
import java.util.Arrays;
import java.util.stream.Collectors;
public class Greet {
String greeting(String... names) {
if ((names == null) || (names.length == 0)) {
return "Hello World!";
} else {
return Arrays.stream(names)
.map(name -> String.format("Hello %s!", name))
.collect(Collectors.joining("\n"));
}
}
}
cat src/com.greetings/main/java/com/greetings/Main.java
package com.greetings;
public class Main {
public static void main(String[] args) {
Greet greet = new Greet();
System.out.println(greet.greeting(args));
}
}
cat src/com.greetings/test/java/com/greetings/GreetTest.java
package com.greetings;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class GreetTest {
@Test
public void greetingNobody_greetsTheWorld() {
Greet greet = new Greet();
assertEquals(
greet.greeting(),
"Hello World!"
);
}
@Test
public void greetingEmptyArray_greetsTheWorld() {
Greet greet = new Greet();
assertEquals(
greet.greeting(new String[0]),
"Hello World!"
);
}
@Test
public void greetingOnePerson_greetsThatPerson() {
Greet greet = new Greet();
assertEquals(
greet.greeting("Alice"),
"Hello Alice!"
);
}
@Test
public void greetingSeveralPeople_greetsAllOfThem() {
Greet greet = new Greet();
assertEquals(
greet.greeting("Alice", "Bob", "Charlie"),
"Hello Alice!\nHello Bob!\nHello Charlie!"
);
}
@Test
public void greetingArrayOfPeople_greetsAllOfThem() {
Greet greet = new Greet();
assertEquals(
greet.greeting(new String[]{"Alice", "Bob", "Charlie"}),
"Hello Alice!\nHello Bob!\nHello Charlie!"
);
}
}
內容其實挺簡單,打印Hello,world
或者打印Hello,人名
,在module-info.java
文件中設置了一個小問題,可以試着填一填,答案在文章末尾。大體內容我們知道了,可是我們應該怎么編譯呢:
javac [3:設置自動化模塊路徑] \
-d mods/main/com.greetings \
src/com.greetings/module-info.java \
src/com.greetings/main/java/com/greetings/Greet.java \
src/com.greetings/main/java/com/greetings/Main.java
答案也在文章末尾,可以自己試着編譯一下。我們來運行一下:
java --module-path mods/main:lib \
--module com.greetings/com.greetings.Main
//Hello World!
java --module-path mods/main:lib \
--module com.greetings/com.greetings.Main \
Alice Bob Charlie
//Hello Alice!
//Hello Bob!
//Hello Charlie!
再來編譯一下GreetingTest
類:
javac --module-path mods:lib \
-d mods/test/com.greetings \
src/com.greetings/module-info.java \
src/com.greetings/main/java/com/greetings/Greet.java \
src/com.greetings/test/java/com/greetings/GreetTest.java
運行一下:
java --module-path mods/main:lib \
--add-modules com.greetings \
--patch-module com.greetings=mods/test/com.greetings \
--module junit/org.junit.runner.JUnitCore \
com.greetings.GreetTest
//JUnit version 4.12
.....
Time: 0.013
OK (5 tests)
完成!將非模塊化jar包轉換成自動模塊化jar包大體就是這樣了,如果遇見不會的java命令,可以通過--help查看說明。
jlink
jlink有什么用呢,其實很簡單,我們可以通過jlink構造自己的jre,前文提到rt.jar包很臃腫,jlink可以幫助我們構建精巧的運行環境。那我們先來看看整個項目的內容,內容很簡單,和第二個小測試一樣,可以快速瀏覽一下,主要是為了練習使用jlink工具的使用:
//結構目錄
src
├── src/com.greetings
│ ├── src/com.greetings/com
│ │ └── src/com.greetings/com/greetings
│ │ └── src/com.greetings/com/greetings/Main.java
│ └── src/com.greetings/module-info.java
└── src/org.astro
├── src/org.astro/module-info.java
└── src/org.astro/org
└── src/org.astro/org/astro
└── src/org.astro/org/astro/World.java
//文件內容
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());
}
}
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";
}
}
那我們編譯一下:
javac -d mods/com.greetings \
src/org.astro/module-info.java \
src/org.astro/org/astro/World.java
javac --module-path mods \
-d mods/org.astro \
src/com.greetings/module-info.java \
src/com.greetings/com/greetings/Main.java
jlink工具目前要求模塊路徑上的模塊以模塊化JAR或JMOD格式打包,所以我們將他們打成jar包:
jar --create \
--file mlib/org.astro@1.0.jar \
--module-version 1.0 \
-C mods/org.astro .
jar --create \
--file mlib/com.greetings.jar \
--main-class=com.greetings.Main \
-C mods/com.greetings .
好了,已經打成模塊化jar包了,開始jlink!
jlink --module-path "${JAVA_HOME}"/jmods:mlib \
--add-modules com.greetings \
--output executable
完成!我們把執行文件輸出到了excutable文件夾,如果對這些命令選項不太理解,可以通過jlink --help
來查看命令解釋。我們現在已經可以執行了:
./executable/bin/java --module-path mods --module com.greetings/com.greetings.Main
//Greetings World!
./executable/bin/java -jar mlib/com.greetings.jar com.greetings.Main
//Greetings World!
是不是很神奇,我們使用了executable中的java命令,對,他就是自定義的jre環境。如果感興趣可以查看一下excutable中的結構。
jmod
jmod可以對模塊或jar包壓縮的一個工具,但他的格式和jar包不一樣。
直接在上個練習目錄中試用命令就好。
jmod create \
--class-path mods/com.greetings:mods/org.astro \
greetings.jmod
然后查看一下:
jmod list greetings.jmod
classes/module-info.class
classes/com/greetings/Main.class
classes/org/astro/World.class
更多的命令解釋可以查看一下jmod --help,我就不多做解釋了。
java9的新功能真的挺有意思的,我這只是冰山一角,想了解更詳細的內容最好去看oracle的文檔,最后附送一張圖,是java9模塊化的小表單。
最后的最后,上面命令的答案想出來了嗎,公布一下結果:
1.requires junit
2.opens com.greetings to junit
3.--module-path lib