1. 程式人生 > >Java9新特性——module模組系統

Java9新特性——module模組系統

這玩意就是一個列表,具體的技術細節需要根據官方文件挖一挖。

modular-模組系統

java9的模組化,從一個獨立的開源專案而來,名為Jigsaw。

為什麼要使用模組化

java開發者都知道,使用java開發應用程式都會遇到一個問題,Jar hell,他就像windows裡的dll hell。

比如我們啟動一個不算大的應用,但一來了很多的jar,如下圖:

輸入圖片說明

這是很常見的。雖然你可以使用 “java -Djava.ext.dirs=lib xxx” 讓命令列小一些,但不可否認,他的classpath就是那麼長。順便說一句,java9中不允許使用extdirs了。

另一方面,jdk本身有很多的api:

輸入圖片說明

對於一些小裝置,它太龐大了。

helloworld

還是習慣先來一個helloworld。在此之前,需要先檢查一下你的java版本:

java -version
java version "9"
Java(TM) SE Runtime Environment (build 9+181)
Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)

如果不是java9,而是 1.8、1.7,那麼慢走不送。

建立主類

首先建立一個java類,就叫Demo吧。

檔案儲存為:src/com/pollyduan/modular/Demo.java

package com.pollyduan.modular;

public class Demo{
    public static void main(String[] args){
        System.out.println("hello modular.");
    }
}

編譯:

$ javac -d classes src/**.java
$ tree .
.
├── classes
│   └── com
│       └── pollyduan
│           └── modular
│               └── Demo.class
└── src └── com └── pollyduan └── modular └── Demo.java

打包jar並執行

$ mkdir lib

$ jar cvf lib/demo.jar -C classes .

$ java --class-path lib/demo.jar com.pollyduan.modular.Demo

hello modular.

–class-path 開關可以簡寫:

$ java -cp lib/demo.jar com.pollyduan.modular.Demo

當然我們可以為jar指定主類,來簡化執行:

Main-Class: com.pollyduan.modular.Demo

需要在MANIFEST.MF 中增加上面一行,即可直接執行:

$ java -jar lib/demo.jar

建立模組

src/module-info.java

module hello{}

我們寫了一個空的模組,命名為hello。

編譯模組

$ javac -d classes src/**.java

反編譯看一下:

$ javap classes/module-info.class
Compiled from "module-info.java"
module hello {
  requires java.base;
}

為什麼我們寫了一個空的模組,反編譯多了一行?先不用管,後面會說明為什麼。

打包模組

$ jar cvf lib/hello.jar -C classes .
$ jar tf lib/hello.jar
META-INF/
META-INF/MANIFEST.MF
module-info.class
com/
com/pollyduan/
com/pollyduan/modular/
com/pollyduan/modular/Demo.class

執行模組

$ java --module-path lib -m hello/com.pollyduan.modular.Demo

hello modular.

這裡和傳統的執行jar不一樣了,這裡不需要classpath,而是module-path。

同樣命令列可以簡寫成:

java -p lib -m hello/com.pollyduan.modular.Demo

模組可以增加Main-Class 嗎?java9的jar提供了一個create開關,用這種方式打包,可以為module指定主類:

$ jar --create --file lib/lib.jar --main-class com.pollyduan.modular.Demo -C classes .

再次執行模組,命令列就會更簡單了。

$ java -p lib -m hello

Jigsaw的設計目標

讓開發者構建和維護一個大型的庫或應用程式更容易;

提高javaSE平臺及JDK實現的安全性和可維護性;

提升應用的效能;

在javase及JDK平臺,讓應用更小以便於部署於更小的計算單元及緊密的雲部署系統。

什麼是modules

為了解決這些問題,jdk在package上層,封裝了一層。

module -> package -> class/interface

那到底 module 是什麼?

module 是一些包的容器。

依賴它的應用稱之為模組,模組是有名字的,其他模組使用該名字使用它。

module匯出特定的包,僅供依賴它的包使用。

module是一個包的容器。module僅僅需要匯出模組依賴的包。

建立一個module

宣告一個module

cat module-info.java

module com.foo.bar{
  exports com.foo.bar.alpha;
  exports com.foo.bar.beta;
}

和package-info.java 類似,它也用一個獨立的java檔案儲存,名為 module-info.java。

建立需要匯出的類

暫時,類的內容不重要,可以先寫一個空類,這裡只列出目錄結構:

$ tree .
.
├── com
│   └── foo
│       └── bar
│           ├── alpha
│           │   └── Alpha.java
│           └── beta
│               └── Beta.java
└── module-info.java

編譯模組

$ javac module-info.java com/foo/bar/alpha/*java com/foo/bar/beta/*java
$ tree .
.
├── com
│   └── foo
│       └── bar
│           ├── alpha
│           │   ├── Alpha.class
│           │   └── Alpha.java
│           └── beta
│               ├── Beta.class
│               └── Beta.java
├── module-info.class
└── module-info.java

打包模組

jar cvf com.foo.bar-1.0.jar .

檢查jar結構:

$ jar tf com.foo.bar-1.0.jar
META-INF/
META-INF/MANIFEST.MF
module-info.class
com/
com/foo/
com/foo/bar/
com/foo/bar/alpha/
com/foo/bar/alpha/Alpha.class
com/foo/bar/beta/
com/foo/bar/beta/Beta.class

引用模組

現在我們已經有了模組 com.foo.bar-1.0.jar,那麼在定義其他模組,就可以使用requires關鍵字引用這個模組了。

module com.foo.app{
  requires co.foo.bar;
  requires java.sql;
}

module com.foo.bar{
  requires com.foo.baz;
  exports com.foo.bar.alpha;
  exports com.foo.bar.beta;
}

module com.foo.baz{
  exports com.foo.baz.mumble;
}

內建的module

jdk原生的包被歸併到內建的module裡,如java.base模組:

module java.base{
  exports java.io;
  exports java.lang;
  exports java.lang.annotation;
  exports java.lang.invoke;
  exports java.lang.module;
  exports java.lang.ref;
  exports java.lang.reflect;
  exports java.lang.math;
  exports java.lang.net;
  //...
}

所有的應用都會預設依賴 java.base,就像以前我們不用顯式的 “import java.lang.*;” 一樣。

這裡驗證了前面helloworld中,為什麼反編譯模組檔案之後會多了一個:”requires java.base;”。

下面的 com.foo.app 模組,不需要顯式地引入java.base:

輸入圖片說明

如果此時com.foo.bar 增加了 com.foo.baz 模組的引用。

輸入圖片說明

那麼,我們知道 com.foo.bar 也隱式 引入了 java.base。

同樣的道理,com.foo.baz 模組也隱式引用了 java.base:

輸入圖片說明

可靠的配置

繼續深入下去,我們知道 java.sql 引用了其他大量的api,那麼下圖就不難理解了。

輸入圖片說明

目前的模組結構,稱為可讀的模組,提供了可靠的配置。

如果引用了不存在的module,和jar一樣,你同樣會觸發 xx not found.

編譯時:

輸入圖片說明

執行時:

輸入圖片說明

可訪問的型別

如果引用的模組沒有匯出某個類,那麼是不可訪問的,這稱為強封裝。

輸入圖片說明

比如 com.foo.bar 模組中有一個內部類BetaImpl:

輸入圖片說明

那麼在 com.foo.bar 模組的主動引用模組 com.foo.app 中如下使用 BeatImpl:

輸入圖片說明

在編譯時,會觸發異常:

輸入圖片說明

就是說:BetaImpl不可訪問,因為包 com.foo.bar.beta.internal 包沒有被匯出。

同樣,即便使用匯出版本編輯成功,而執行時引用了未匯出版本模組:

輸入圖片說明

檢視內建的模組

$ jmod list $JAVA_HOME/jmods/java.base.jmod
classes/module-info.class
classes/apple/security/AppleProvider$1.class
...
classes/java/lang/Object.class
...
bin/java
bin/keytool
...
conf/security/java.policy
...

檢視更多內建模組:

$ java --list-modules

java.activation@9
java.base@9
java.compiler@9
java.corba@9
java.datatransfer@9
java.desktop@9
//...節省篇幅略

helloworld進階

從helloworld的基礎上,增加一個模組的依賴。

先來回顧一下helloworld的目錄結構:

$ tree module
module
├── classes
│   ├── com
│   │   └── pollyduan
│   │       └── modular
│   │           └── Demo.class
│   └── module-info.class
├── lib
│   ├── demo.jar
│   ├── hello.jar
└── src
    ├── com
    │   └── pollyduan
    │       └── modular
    │           └── Demo.java
    └── module-info.java

增加一個模組service,其中service目錄和module目錄同級。

$ tree service
service
├── classes
├── lib
└── src
    └── com
        └── pollyduan
            └── service

建立服務類

service/src/com/pollyduan/service/HelloService.java

package com.pollyduan.service;

public class HelloService{
    public void sayHi(String name){
        System.out.println("Hello "+name);
    }
}

宣告service模組

service/src/module-info.java

module service{
    exports com.pollyduan.service;
}

編譯service模組

$ javac -d service/classes service/src/**java

$ tree service/classes/
service/classes/
├── com
│   └── pollyduan
│       └── service
│           └── HelloService.class
└── module-info.class

打包service模組

jar --create --file service/lib/service.jar -C service/classes/ .

修改helloworld模組

module/src/module-info.java

module hello{
    requires service;
}

修改helloworld主類使用service中的方法

module/src/com/pollyduan/modular/Demo.java

package com.pollyduan.modular;
import com.pollyduan.service.HelloService;

public class Demo{
    public static void main(String[] args){
        new HelloService().sayHi("java9 modular.");
    }
}

重新編譯打包helloworld

$ javac -p service/lib -d module/classes module/src/**java

$ jar --create --file module/lib/hello.jar -p service/lib --main-class com.pollyduan.modular.Demo -C module/classes .

$ java -p module/lib:service/lib -m hello
Hello java9 modular.

打完收工。

模組相關的工具

模組整理工具,用於將一系列模組聚合、優化,打包到一個自定義的映象中。這裡說的是jre映象,不是jar。

輸入圖片說明

如果我們只引用了java.base 模組,那麼可以打包時可以選擇性地打包:

$ jlink -p $JAVA_HOME/jmods --add-modules java.base --output jre

這時輸出的jre就是一個完整可用的jre,他和原生jdk的大小相差很大:

$ du -sh $JAVA_HOME jre
493M    /Library/Java/JavaVirtualMachines/jdk-9.jdk/Contents/Home
 35M    jre

這樣,我們可以把自己的模組也打包進去。

$ mkdir jmods
$ jmod create --class-path service/lib/service.jar jmods/service.jmod
$ jmod create --class-path module/lib/hello.jar jmods/module.jmod
$ jlink -p $JAVA_HOME/jmods:jmods --add-modules java.base,hello --output jre

$ cat jre/release
JAVA_VERSION="9"
MODULES="java.base service hello"

./jre/bin/java --list-modules
hello
java.base@9
service

注意,module-path的值採用classpath同樣的分隔符,如windows裡的分號和linux裡的冒號;而add-modules 開關的值是使用逗號分隔的。

這樣,我們打包了一個只有30M的jre,而且,把自己的module也打包進去了。然後呢?直接執行模組看看:

$ ./jre/bin/java -m hello

Hello java9 modular.

jlink還提供了一個launcher開關,可以將我們的模組編譯成和java命令一樣的可執行檔案,放在 jre/bin 中。

$ jlink -p $JAVA_HOME/jmods:jmods --add-modules java.base,hello --launcher Hello=hello --output jre

$ ls jre/bin
Hello   java    keytool

$ ./jre/bin/Hello
Hello java9 modular.

請留意launcher的格式——”[命令]=[模組]”,為了區分,命令使用了首字母大寫。

jlink的開關很多,功能不僅於此,如下可以將已經很小的jre繼續壓縮:

$ jlink -p $JAVA_HOME/jmods:jmods --add-modules java.base,hello --launcher Hello=hello \
--compress 2 --strip-debug \
--output jre_mini

$ du -sh jre*
 35M    jre
 21M    jre_mini

jdeps

這是一個java類檔案的依賴分析器。

$ jdeps --module-path service/lib module/lib/hello.jar
hello
 [file:///Users/pollyduan/tmp/java/java9/module/lib/hello.jar]
   requires mandated java.base (@9)
   requires service
hello -> java.base
hello -> service
   com.pollyduan.modular                              -> com.pollyduan.service                              service
   com.pollyduan.modular                              -> java.lang                                          java.base

jmod

用於建立jmod檔案,以及檢視已存在的jmod檔案。

建立jmod檔案:

$ jmod create --class-path . com.foo.bar.jmod
$ jmod list com.foo.bar.jmod
classes/module-info.class
classes/.com.foo.bar.jmod.tmp
classes/com/foo/bar/alpha/Alpha.class
classes/com/foo/bar/alpha/Alpha.java
classes/com/foo/bar/beta/Beta.class
classes/com/foo/bar/beta/Beta.java
classes/com.foo.bar-1.0.jar
classes/module-info.java

jdeprscan

這是一個針對jar的靜態的分析工具,查詢其依賴的api。

$ jdeprscan dom4j-1.6.1.jar
Jar 檔案 dom4j-1.6.1.jar:
class org/dom4j/bean/BeanMetaData 使用已過時的方法 java/lang/Integer::<init>(I)V
錯誤: 找不到類 org/relaxng/datatype/DatatypeException
錯誤: 找不到類 com/sun/msv/datatype/xsd/XSDatatype
錯誤: 找不到類 com/sun/msv/datatype/DatabindableDatatype
錯誤: 找不到類 com/sun/msv/datatype/SerializationContext
錯誤: 找不到類 com/sun/msv/datatype/xsd/TypeIncubator
錯誤: 找不到類 com/sun/msv/datatype/xsd/DatatypeFactory
class org/dom4j/io/SAXEventRecorder 使用已過時的方法 java/lang/Integer::<init>(I)V
class org/dom4j/io/SAXHelper 使用已過時的類 org/xml/sax/helpers/XMLReaderFactory
class org/dom4j/io/SAXReader 使用已過時的類 org/xml/sax/helpers/XMLReaderFactory
錯誤: 找不到類 org/xmlpull/v1/XmlPullParserFactory
錯誤: 找不到類 org/xmlpull/v1/XmlPullParser
錯誤: 找不到類 org/gjt/xpp/XmlPullParserFactory
錯誤: 找不到類 org/gjt/xpp/XmlPullParser
錯誤: 找不到類 org/jaxen/XPath
錯誤: 找不到類 org/jaxen/VariableContext
class org/dom4j/tree/NamespaceCache 使用已過時的方法 java/lang/Integer::<init>(I)V
class org/dom4j/tree/NamespaceCache 使用已過時的方法 java/lang/Float::<init>(F)V
錯誤: 找不到類 org/jaxen/NamespaceContext
錯誤: 找不到類 org/jaxen/SimpleNamespaceContext
錯誤: 找不到類 org/jaxen/dom4j/Dom4jXPath
錯誤: 找不到類 org/jaxen/JaxenException
錯誤: 找不到類 org/jaxen/pattern/Pattern
錯誤: 找不到類 org/jaxen/Context
錯誤: 找不到類 org/jaxen/pattern/PatternParser
錯誤: 找不到類 org/jaxen/saxpath/SAXPathException
錯誤: 找不到類 org/jaxen/ContextSupport
錯誤: 找不到類 org/jaxen/XPathFunctionContext
錯誤: 找不到類 org/jaxen/SimpleVariableContext
錯誤: 找不到類 org/jaxen/dom4j/DocumentNavigator
錯誤: 找不到類 org/gjt/xpp/XmlStartTag

模組小結

關鍵詞

模組定義 module-info.java
模組描述符 module-info.class
modular jar files 模組jar檔案
jmod files 模組清單檔案
observable modules 
readable modules => reliable configuration 可靠配置
accessible types => strong encapsulation 強封裝

module和jar的區別

jar 實際上就是一個類檔案的集合,就像一個zip文件;而module是一個規範的java元件,除了jar還有更多的工具支援。

jar中的資源可以任意使用;而module中的資源只有匯出的才可以使用。

module仍然以jar為載體。

物理層面上,module在一定意義上可以理解為jar中的一個module-info.class

目錄結構的變化,以前一個jar專案是:
project
├── bin
├── classes
└── src
而module專案則是:
project
├── module1
│   ├── classes
│   ├── lib
│   └── src
└── module2
    ├── classes
    ├── lib
    └── src

模組需要注意的問題

module 的依賴,同樣存在迴圈依賴問題,需要注意。如:模組A,requires B; 模組B有 requires A。

IDE是否支援?傳統的IDE都是基於classpath管理專案,現在需要支援基於module-path

module打包的jar,你仍然可以當做普通jar來用,沒有人阻止你,至少目前是這樣的。不過這並不是說module完全沒有意義,就像class檔案中的成員設定為私有,不允許外部訪問,你完全可以通過反射去訪問它,一個道理。

模組的應用場景

首先,最突出的用法,就是使用jlink打包自定義的映象,分發到小計算單元中執行,如docker,嵌入式裝置。

其次,將來必定會有越來越多的容器來支援直接執行模組。

然後,他對於應用的熱插拔的外掛場景中,會有一席之地。

最後,就是代替jar方式執行的模組執行方式。

拭目以待。