2

jdk9模块快速入门

列出自带模块:java --list-modules
mac多版本jdk共存:http://adolphor.com/blog/2016...
模块规则示意图:
clipboard.png

incubator modules:孵化模块 以jdk.incubator开头,比如jdk.incubator.httpclient(jdk11之后这是正式的模块了:[java.net.http][1],具体参考:http://openjdk.java.net/jeps/...

module descriptor:模块描述文件 module-info.java 例如java.prefs的模块描述文件内容:

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

requires代表依赖的模块,只有依赖的模块存在才能通过编译并运行.需要注意的是,所有模块均自动隐式依赖java.base模块,不需要显示声明
exports指出需要暴露的包,如果某个包没有被exports,那么其他模块是无法访问的。

两个名词:Readability,Accessibility

Readability:指的是必须exports的包才可被其他模块访问
Accessibility:指的是即使是exports的包,其中的类的可访问下也要基于java的访问修饰符,仅有public修饰的才可被其他模块访问

Implied Readability(隐式Readability, requires transitive):
Readability默认情况下是不会被传递的, 比如Maven中,我们知道依赖可以被传递,但是module的requires不会被传递,比如: 下图中java.desktop无法访问java.xml模块中exports的包,虽然它引用了java.prefs模块
clipboard.png

但是如果你将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的内容。

Qualified Exports(有限制的exports)
比如我只想exports某个包给部分模块,而不是所有模块

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去查询。

在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

查看已有模块描述

查看已有模块描述有两种方式:
一、直接看module-info.java
二、使用命令 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
1、提供模块描述中使用provides

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

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

2、消费模块描述中使用uses

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

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

3、消费模块代码中使用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、我只关心某一个实现,如何标识获取;2、我只想获取某一个特定实现,但我不想在遍历中初始化其他实例;
问题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》


xbynet
1k 声望124 粉丝

不雨花犹落,无风絮自飞