Java9模塊化學習筆記一之快速入門

jdk9模塊快速入門

列出自帶模塊:java --list-modules
mac多版本jdk共存:http://adolphor.com/blog/2016...
模塊規則示意圖:
clipboard.pnghtml

incubator modules:孵化模塊 以jdk.incubator開頭,好比jdk.incubator.httpclient(jdk11以後這是正式的模塊了:[java.net.http][1],具體參考:http://openjdk.java.net/jeps/...java

module descriptor:模塊描述文件 module-info.java 例如java.prefs的模塊描述文件內容:git

module java.prefs{
    requires java.xml;
    exports java.util.prefs;
}

requires表明依賴的模塊,只有依賴的模塊存在才能經過編譯並運行.須要注意的是,全部模塊均自動隱式依賴java.base模塊,不須要顯示聲明
exports指出須要暴露的包,若是某個包沒有被exports,那麼其餘模塊是沒法訪問的。github

兩個名詞:Readability,Accessibility

Readability:指的是必須exports的包纔可被其餘模塊訪問
Accessibility:指的是即便是exports的包,其中的類的可訪問下也要基於java的訪問修飾符,僅有public修飾的纔可被其餘模塊訪問sql

Implied Readability(隱式Readability, requires transitive):
Readability默認狀況下是不會被傳遞的, 好比Maven中,咱們知道依賴能夠被傳遞,可是module的requires不會被傳遞,好比: 下圖中java.desktop沒法訪問java.xml模塊中exports的包,雖然它引用了java.prefs模塊
clipboard.pngmacos

可是若是你將requires改爲requires transitive的話,那麼傳遞性依賴就能夠生效了。
有個時候咱們定義API模塊時常常忘記哪些該requires transitive,這時該怎麼辦呢? 模塊編譯時使用-Xlint:exports選項,它會檢測出這些問題並warn。
Aggregate Module(聚合模塊)
因爲requires transitive的存在,就能夠支持聚合模塊。有些聚合模塊能夠沒有任何代碼,就一個module-info.java描述文件,好比java.se, java.se.ee模塊
不建議直接引用java.se模塊,由於它就至關於java9之前版本的rt.jar的內容。windows

Qualified Exports(有限制的exports)
好比我只想exports某個包給部分模塊,而不是全部模塊api

exports com.sun.xml.internal.stream.writers to java.xml.ws,java.sql;

Qualified Exports不建議普通模塊使用,java platform使用它主要是爲了減小內部重複代碼,通常用它暴露internal內部使用的一些類給部分模塊。框架

module path
module path相似於classpath,只不過它只包括module
module resolution支持定義模塊依賴圖,根據root module去查詢。ide

在modular jdk中以非模塊化方式開發
在java9中也是容許你以非模塊化方式開發和運行應用的(也就是說,模塊化開發是可選的),若是你的應用中沒有module-info.java,那麼這就是一個unnamed module. java9對於unnamed module的處理方式就是全部的jdk模塊均直接可用(模塊圖中是以java.se模塊做爲root模塊的,也意味着單獨處於java.se.ee下的一些包,好比JAXB API是沒法訪問到的)。
可是須要注意的是,在java8以及以前的版本中,咱們能夠訪問jdk中的一些不推薦訪問的內部類,好比com.sun.image.codec.jpeg,但在java9模塊化以後被強封裝了,因此在java9中沒法使用這些內部類,也就是說沒法經過編譯,可是java9爲了保持兼容性,容許以前引用這些內部類的已有的jar或已編譯的類正確運行。換言之,就是java9不容許源碼中引用這些類,沒法經過編譯,可是以前版本中引用這些類的已編譯class文件是容許正常運行的。

模塊結構

MacBook-Pro:easytext-singlemodule tjw$ tree
.
├── README.md
├── out
│   └── easytext
│       ├── javamodularity
│       │   └── easytext
│       │       └── Main.class
│       └── module-info.class
├── run.sh
└── src
    └── easytext
        ├── javamodularity
        │   └── easytext
        │       └── Main.java
        └── module-info.java

其中區別於傳統src目錄,模塊src目錄下首先是模塊目錄easytext,這名字與module-info.java裏的保持一致,模塊目錄下包含源碼及module-info.java模塊描述文件。src/easytext是源碼目錄, javamodularity/easytext是包

編譯文件run.sh內容:

mkdir -p out

javac -d out --module-source-path src -m easytext
java --module-path out -m easytext/javamodularity.easytext.Main

打包成jar

jar -cfe out/easytext.jar javamodularity.easytext.Main -C out/easytext .

-cf不用說了,-e指定入口類,-C指定須要打包進jar的文件路徑
運行模塊jar:

java --module-path out -m easytext

請注意,這與直接運行模塊目錄的區別:java --module-path out -m easytext/javamodularity.easytext.Main, 運行模塊jar因爲咱們打包時經過-e選項指定了入口類,這時-m只須要指定模塊名便可運行。

若是你想查看運行時模塊的加載過程:java --show-module-resolution --limit-modules java.base --module-path out -m easytext
輸出結果: 表示easytext爲root模塊,因爲我限制了java.base再也不往下輸出了,而咱們模塊又沒有別的額外依賴,因此僅有這行輸出。

root easytext file:///Users/tjw/learn/java-9-moodularity-examples/chapter3/easytext-singlemodule/out/easytext.jar

假如我去掉--limit-modules限制,運行,則輸出以下:java --show-module-resolution --module-path out -m easytext

root easytext file:///Users/tjw/learn/java-9-moodularity-examples/chapter3/easytext-singlemodule/out/easytext.jar
java.base binds jdk.localedata jrt:/jdk.localedata
java.base binds jdk.charsets jrt:/jdk.charsets
java.base binds jdk.jlink jrt:/jdk.jlink
java.base binds jdk.jartool jrt:/jdk.jartool
java.base binds jdk.jdeps jrt:/jdk.jdeps
java.base binds jdk.compiler jrt:/jdk.compiler
java.base binds jdk.javadoc jrt:/jdk.javadoc
java.base binds jdk.packager jrt:/jdk.packager
java.base binds java.desktop jrt:/java.desktop
java.base binds jdk.crypto.cryptoki jrt:/jdk.crypto.cryptoki
java.base binds java.naming jrt:/java.naming
java.base binds jdk.crypto.ec jrt:/jdk.crypto.ec
java.base binds java.xml.crypto jrt:/java.xml.crypto
java.base binds java.security.jgss jrt:/java.security.jgss
java.base binds java.security.sasl jrt:/java.security.sasl
java.base binds jdk.deploy jrt:/jdk.deploy
java.base binds java.smartcardio jrt:/java.smartcardio
java.base binds jdk.security.jgss jrt:/jdk.security.jgss
java.base binds java.logging jrt:/java.logging
java.base binds jdk.security.auth jrt:/jdk.security.auth
java.base binds java.management jrt:/java.management
java.base binds jdk.zipfs jrt:/jdk.zipfs
jdk.security.auth requires java.security.jgss jrt:/java.security.jgss
jdk.security.auth requires java.naming jrt:/java.naming
jdk.security.jgss requires java.security.jgss jrt:/java.security.jgss
jdk.security.jgss requires java.security.sasl jrt:/java.security.sasl
jdk.security.jgss requires java.logging jrt:/java.logging
jdk.deploy requires jdk.unsupported jrt:/jdk.unsupported
jdk.deploy requires java.desktop jrt:/java.desktop
jdk.deploy requires java.naming jrt:/java.naming
jdk.deploy requires java.scripting jrt:/java.scripting
jdk.deploy requires java.prefs jrt:/java.prefs
jdk.deploy requires java.logging jrt:/java.logging
jdk.deploy requires java.rmi jrt:/java.rmi
jdk.deploy requires java.xml jrt:/java.xml
jdk.deploy requires java.management jrt:/java.management
java.security.sasl requires java.logging jrt:/java.logging
java.security.jgss requires java.naming jrt:/java.naming
java.xml.crypto requires java.logging jrt:/java.logging
java.xml.crypto requires java.xml jrt:/java.xml
java.naming requires java.security.sasl jrt:/java.security.sasl
jdk.crypto.cryptoki requires jdk.crypto.ec jrt:/jdk.crypto.ec
java.desktop requires java.datatransfer jrt:/java.datatransfer
java.desktop requires java.xml jrt:/java.xml
java.desktop requires java.prefs jrt:/java.prefs
jdk.packager requires java.logging jrt:/java.logging
jdk.packager requires java.desktop jrt:/java.desktop
jdk.packager requires java.xml jrt:/java.xml
jdk.packager requires jdk.jlink jrt:/jdk.jlink
jdk.javadoc requires jdk.compiler jrt:/jdk.compiler
jdk.javadoc requires java.xml jrt:/java.xml
jdk.javadoc requires java.compiler jrt:/java.compiler
jdk.compiler requires java.compiler jrt:/java.compiler
jdk.jdeps requires jdk.compiler jrt:/jdk.compiler
jdk.jdeps requires java.compiler jrt:/java.compiler
jdk.jlink requires jdk.internal.opt jrt:/jdk.internal.opt
jdk.jlink requires jdk.jdeps jrt:/jdk.jdeps
java.prefs requires java.xml jrt:/java.xml
java.rmi requires java.logging jrt:/java.logging
java.management binds java.management.rmi jrt:/java.management.rmi
java.management binds jdk.management.cmm jrt:/jdk.management.cmm
java.management binds jdk.management.jfr jrt:/jdk.management.jfr
java.management binds jdk.management jrt:/jdk.management
java.management binds jdk.internal.vm.compiler.management jrt:/jdk.internal.vm.compiler.management
java.scripting binds jdk.scripting.nashorn jrt:/jdk.scripting.nashorn
java.naming binds jdk.naming.dns jrt:/jdk.naming.dns
java.naming binds jdk.naming.rmi jrt:/jdk.naming.rmi
java.datatransfer binds java.desktop jrt:/java.desktop
java.compiler binds jdk.compiler jrt:/jdk.compiler
java.compiler binds jdk.javadoc jrt:/jdk.javadoc
jdk.naming.rmi requires java.rmi jrt:/java.rmi
jdk.naming.rmi requires java.naming jrt:/java.naming
jdk.naming.dns requires java.naming jrt:/java.naming
jdk.scripting.nashorn requires java.logging jrt:/java.logging
jdk.scripting.nashorn requires java.scripting jrt:/java.scripting
jdk.scripting.nashorn requires jdk.dynalink jrt:/jdk.dynalink
jdk.internal.vm.compiler.management requires jdk.internal.vm.ci jrt:/jdk.internal.vm.ci
jdk.internal.vm.compiler.management requires jdk.internal.vm.compiler jrt:/jdk.internal.vm.compiler
jdk.internal.vm.compiler.management requires jdk.management jrt:/jdk.management
jdk.internal.vm.compiler.management requires java.management jrt:/java.management
jdk.management requires java.management jrt:/java.management
jdk.management.jfr requires jdk.jfr jrt:/jdk.jfr
jdk.management.jfr requires java.management jrt:/java.management
jdk.management.jfr requires jdk.management jrt:/jdk.management
jdk.management.cmm requires jdk.management jrt:/jdk.management
jdk.management.cmm requires java.management jrt:/java.management
java.management.rmi requires java.management jrt:/java.management
java.management.rmi requires java.naming jrt:/java.naming
java.management.rmi requires java.rmi jrt:/java.rmi
jdk.dynalink requires java.logging jrt:/java.logging
jdk.internal.vm.compiler requires jdk.internal.vm.ci jrt:/jdk.internal.vm.ci
jdk.internal.vm.compiler requires jdk.unsupported jrt:/jdk.unsupported
jdk.internal.vm.compiler requires java.instrument jrt:/java.instrument
jdk.internal.vm.compiler requires jdk.management jrt:/jdk.management
jdk.internal.vm.compiler requires java.management jrt:/java.management
jdk.internal.vm.ci binds jdk.internal.vm.compiler jrt:/jdk.internal.vm.compiler
jdk.dynalink binds jdk.scripting.nashorn jrt:/jdk.scripting.nashorn

模塊路徑
模塊路徑格式支持三種:大目錄下面有多個模塊的情形,好比上述的out目錄;模塊目錄自己;模塊jar
模塊路徑分隔符,遵循系統的Path.sep;好比mac/Linux下是 : ,windows下是 ;
--module-path 能夠簡寫爲 -p 如 java -p

Linking Modules

jlink工具容許你建立一個運行時鏡像runtime image,包含java應用容許所需的最小集合。而不是像以前那樣須要打包整個jre.

$JAVA_HOME/bin/jlink --module-path out/:$JAVA_HOME/jmods \
        --add-modules easytext \
        --launcher easy=easytext \
        --output easytext-image

須要注意的是,默認狀況下jlink沒有被添加到PATH中,須要你手動添加一下

跟javac和java命令不一樣,jlink須要指定jdk平臺的模塊路徑$JAVA_HOME/jmods
--add-modules 指定easytext爲root模塊
--launcher指定啓動的入口爲easytext模塊,easy是生成的啓動文件名
--output 指定生成的鏡像路徑

最終生成的鏡像文件內容:

easytext-image/
├── bin/
│   ├── easy
│   ├── java
│   └── keytool
├── conf
│   └── security
│       └── policy
│           ├── limited
│           └── unlimited
├── include
│   └── darwin
├── legal
│   └── java.base
└── lib
    ├── jli
    ├── security
    └── server

試了下,打包成壓縮文件後,只有12M大小:

-rw-r--r--  1 tjw  staff    12M  6 29 13:32 easy.tar.gz

查看已有模塊描述

查看已有模塊描述有兩種方式:
1、直接看module-info.java
2、使用命令 java --describe-module javafx.controls

模塊對於GUI應用的問題:

注意:Jdk11已經將javafx從平臺模塊中移除

module easytext.gui {

   exports javamodularity.easytext.gui to javafx.graphics;

   requires javafx.graphics;
   requires javafx.controls;
   requires easytext.analysis;
}

exports javamodularity.easytext.gui to javafx.graphics;是必須的,由於gui應用中Application會反射訪問具體的實現,那麼這就意味着javafx.graphics.Application的運行時須要可以訪問咱們的應用模塊的Application實現。

clipboard.png

思考一個問題:如何確保模塊之間的解耦,經過定義一個接口模塊能夠分離實現,可是如何確保自動加載哪一個實現呢?

Services

services的設計有點相似於ioc,概念上分爲服務提供者和服務消費者。
主要入口類就是java.util.ServiceLoader,這個類在jdk6的時候就已經存在,不過在jdk9進行了改造以支持模塊化,jdk9以前ServiceLoader主要是用來使jdk更加插件化,一些框架好比dubbo也會使用ServiceLoader來作插件化工做。 jdk9以前services的提供是在jar包下的META-INF/services目錄下的一個文本文件,文件名爲服務接口的全限定類名,如:com.test.HelloWorld,文件內容也爲服務實現的全限定類名com.test.HelloWorldImpl。好比dubbo的filter

jdk9改造後使得ServiceLoader支持模塊化service加載,已達到模塊間面向接口,使實現解耦的目的。注意:服務提供模塊能夠不用exports服務實現。
步驟:
0、服務接口模塊定義
如模塊easytext.analysis.api
一、提供模塊描述中使用provides

module easytext.analysis.coleman {
   requires easytext.analysis.api;

   provides javamodularity.easytext.analysis.api.Analyzer with javamodularity.easytext.analysis.coleman.Coleman;
}

二、消費模塊描述中使用uses

module easytext.cli {
   requires easytext.analysis.api;

   uses javamodularity.easytext.analysis.api.Analyzer;
}

三、消費模塊代碼中使用ServiceLoader類加載

Iterable<Analyzer> analyzers = ServiceLoader.load(Analyzer.class); //其實ServiceLoader.load返回的是一個ServiceLoader實例,只不過它實現了Iterable接口

      for (Analyzer analyzer: analyzers) { 
        System.out.println(analyzer.getName() + ": " + analyzer.analyze(sentences));
      }

Services生命週期

ServiceLoader.load是懶加載的
ServiceLoader.load每調用一次都會返回一個ServiceLoader實例,獲取的服務實例也是新的,跟Spring等容器不一樣,它不存在單例模式。這就須要注意,千萬不要經過服務實例來共享狀態。
服務實現類要麼提供無參構造器,要麼提供public static provider()方法,返回實例。

public static ExampleProviderMethod provider() {
    return new ExampleProviderMethod("Analyzer created by static method");
}

結合java8接口靜態方法實現service工廠模式:

public interface Analyzer {

   String getName();

   double analyze(List<List<String>> text);

   static Iterable<Analyzer> getAnalyzers() {
     return ServiceLoader.load(Analyzer.class); // <1>
   }

}
module easytext.analysis.api {
   exports javamodularity.easytext.analysis.api;

   uses javamodularity.easytext.analysis.api.Analyzer;
}

好處:ServiceLoader的調用及模塊uses聲明都統一在api模塊中定義。

Service Type及延遲初始化再談

問題: 上面所述的獲取服務接口實現的方式只能遍歷Itreable,而遍歷後全部的實現都會被初始化。兩個問題:一、我只關心某一個實現,如何標識獲取;二、我只想獲取某一個特定實現,但我不想在遍歷中初始化其餘實例;
問題1:我只關心某一個實現,如何標識獲取
方案1:在服務接口中添加標識方法,好比getName,而後消費者遍歷時經過getName的值來判斷。這樣不少場景下是可行的。可是也有些場景須要經過別的方式來標識,好比是否實現某個抽象類,是否被某個註解標註,這種狀況下就須要下面的方案2。
方案2: Java9對ServiceLoader API進行了強化,提供ServiceLoader.Provider stream. ServiceLoader.Provider能夠在不實例化實現以前對實現類進行反射檢索。
好比下面的就是經過檢索被@Fast註解標註的實現。

public class Main {
  public static void main(String args[]) {
    ServiceLoader<Analyzer> analyzers =
      ServiceLoader.load(Analyzer.class);

    analyzers.stream()
      .filter(provider -> isFast(provider.type()))
      .map(ServiceLoader.Provider::get) 
      .forEach(analyzer -> System.out.println(analyzer.getName()));
  }

  private static boolean isFast(Class<?> clazz) {
    return clazz.isAnnotationPresent(Fast.class)
      && clazz.getAnnotation(Fast.class).value() == true;

  }
}

注意上述代碼中的provider.type(),它返回的是實現類的Class對象,這裏咱們要注意,咱們的實現類是沒有被exports的,但經過provider.type()是能夠獲取到。可是咱們能夠調用provider.type().newInstance()嗎?不能夠,由於它仍然遵照模塊的封裝約定。若是強行調用會報IllegalAccessError異常。

Service Moudle Resolution And Linking

對於Service,模塊解析方式和以前的相同:從root模塊開始,解析requires,而後解析uses,而後會把uses對應的provides模塊都解析到resolved module sets中。
**可是對於jlink鏡像打包而言,它不會把Service的provides模塊打包進去(一個直接的緣由就是java.base中使用了大量的uses),
因此使用jlink打包時須要注意經過--add-modules添加provides。** 固然若是你不知道有哪些provids模塊,能夠經過jlink選項 --suggest-providers 接口名查看

$JAVA_HOME/bin/jlink --module-path mods/:$JAVA_HOME/jmods --add-modules main--suggest-providers javamodularity.easytext.analysis.api.Analyzer

建議的提供方:
  provider.factory.example provides javamodularity.easytext.analysis.api.Analyzer used by main
  provider.method.example provides javamodularity.easytext.analysis.api.Analyzer used by main

不過若是provide模塊也uses別的模塊,那麼也須要照樣分析,並根據須要--add-modules添加進來。

不過還有個替代方式,添加--bind-services選項,添加後jlink會解析uses及provides,但不推薦使用,由於java.base也使用了大量uses,會致使打包後鏡像很大

源碼見書籍源碼:https://github.com/java9-modu...

主要參考書籍:《Java 9 Modularity Patterns and Practices for Developing Maintainable Applications》

相關文章
相關標籤/搜索