Java 9 模块化(Modularity)

JDK9的发布一直在推迟,终于在2017年9月21日发布了。下面是JDK9的几个下载地址:
JDK9.0.1 Windows-x64下载地址
Oracle Java 官网下载地址
OpenJDK 9官网
OpenJDK JDK9下载

从安装的JDK9文件夹下会发现没有jre文件夹了,并且多了一个jmods文件夹,想想为什么?
传统的jar文件是在运行时runtime使用,而 .jmods文件是在开发时development time使用。

这一次,Java9带来的模块化(Modularity)是一次重大的改变。对于在此之前(Java8及以前)的Java发布版本所添加的新特性,你都可以随意使用,但是Java9不同,这次Java9的平台模块化系统(Java Platform Modular System)在思考、设计、编写Java应用程序方面都是一个全新的改变。

1.Java 9 模块化简介

模块化是Java9发布版本最重要最强大的改变,此外Java9还带来了许多新的改变,例如支持HTTP2.0、交互式Shell(叫做jshell)等。那么模块化究竟会带来什么好处呢?为何要引入模块化?
模块(module)可以是任意东西,从一组代码实体、组件或UI类型到框架元素再到完整的可重用的库。
模块化在软件开发中通常要到达两个目标:

 - 分而治之(Divide and conquer approach):对于非常大的问题通常需要将大问题分解成一个个的小问题,然后单独解决它们。
 - 实现具有封装性和明确定义的接口:模块化后就可以隐藏模块的内部实现(称为封装encapsulation),同时暴露给用户的东西称为接口(interface)。

现在回顾下封装:
private 修饰成员变量和方法,封装的边界是Class(类)
protected 修饰成员变量和方法,封装的边界是Package(包)
无修饰符 修饰成员变量和方法或类型(Types),封装的边界是Package(包)
对于封装,难道有这些还不够吗?上面这些修饰符都集中在控制访问成员变量和方法上面。而对于类型(types)的访问保护(封装)只能让它在包层级保护package-protected。模块化可以在更大的粒度上进行封装,对类型的保护变成private。
来看几个没有模块化带来的问题的案例:

1.1 无法隐藏内部API和类型

为了更好的重用一个字符串排序的工具类,将它打包成一个jar文件,它又两个包组成:acme.util.stringsorter和acme.util.stringsorter.internal。
前者包含只有一个方法sortStrings(List)的类StringSorterUtil,后者包含只有一个方法sortStrings(List)的BubbleSortUtil类,BubbleSortUtil类用的是著名的Bubble排序算法对给定的字符串排序,调用StringSorterUtil的sortStrings方法实际上是反向代理执行BubbleSortUtil类的sortStrings方法。
这里写图片描述
后来jar包开发者发现哈希Hash排序算法更优于Bubble排序算法,于是升级了下,将HashSortUtil类加到了acme.util.stringsorter.internal包下面并移除掉了原来的BubbleSortUtil类。幸好单独有这么个internal包,用户调用StringSorterUtil的sortStrings方法的方式没有改变,那用户就可以直接升级这个jar包了。一切是多么的美好。
但是!还是出问题了。
jar包作者本意是acme.util.stringsorter.internal包不让用户使用,是private的,但是当用户将这个jar包加到classpath后,仍然可以直接使用BubbleSortUtil类,这并不是jar包开发者所希望的。现在升级了版本后,那些直接使用BubbleSortUtil类的应用由于找不到BubbleSortUtil类连编译都通不过。显然,即使将包命名为internal还是无法避免用户去访问它。
Java平台内部API是不建议使用的,尽管官方给出了提醒,但还是无法避免开发者使用,现在在Java9中已经将其隐藏了,例如以sun开头的包。
那么有没有什么方式来封装这些internal类呢?模块!

1.2 可靠性问题

应用启动运行了几个小时候没有发生错误,但是,并不能说之后就没有问题。比如或许有一个类还没有被执行到,当执行到它时,JVM发现找不到这个类的一个import,抛出类找不到异常。又或许同一个类的多个版本加到了类路径而JVM只选择了它找到的第一个副本。难道没有一个更好的方式来确保任意的Java应用不需要执行就将会可靠reliably地运行?模块描述符!

1.3 类路径classpath问题

Jar文件仅仅是将一组类方便的放在一起而已。一旦加入到classpath中,JVM就对Jar中的所有classes一视同仁放到同一个根root目录下,而不管这些class文件位置在哪。想象一个应用的成千上万的类放置在同一个目录下而没有结构的样子,这对于管理和维护将是一场噩梦。代码库越大,问题越大。例如有20悠久历史的Java平台本身!!!
Java 1996年发布的第一个版本至少有500个public类,到2014年发布的JDK8已经达到4200多个public类和20000多个文件。传统地,每一个JRE在运行时都要从一个库加载所有的类,这个库就是rt.jar,rt的意思是Run Time.
Java9之前,每一个runtime自带开箱即用的所有编译好的平台类,这些类被一起打包到一个JRE文件叫做rt.jar。你只需将你的应用的类放到classpath中,这样runtime就可以找到,而其它的平台类它就简单粗暴的从rt.jar文件中去找。尽管你的应用只用到了这个庞大的rt.jar的一部分,这对JVM管理来说不仅增加了非必要类的体积,还增加了性能负载。
Java8的rt.jar大概 60 MB大小,目前尚可忍受,但如果一直这样下去想象之后它的体积肯定会越来越庞大,难道没有更好的方式来运行java吗?Java9模块化可以按需自定义runtime!这也就是jdk9文件夹下没有了jre目录的原因!

1.4 Java Platform Module System(JPMS)

JPMS(JAVA平台模块化系统)引入了一个新的语言结构来构建可重用的组件,称为模块modules。在Java9 的模块中,你可以将某些类型types和包packages组合到一个模块module中,并给模块提供如下3个信息:

  • 名称:模块的唯一的名字,例如 com.acme.analytics,类似于包名。
  • 输入:什么是模块需要和使用到的?什么是模块编译和运行所必需的?
  • 输出:什么是模块要输出或暴露给其他模块的?

默认地,一个模块中的每一个java类型只能被该模块中的其他类型所访问。要想暴露类型给外部的模块使用,需要明确指定哪些包packages要暴露export。任何模块只能在包的层级上暴露,一旦暴露了某个包,那这个包中的所有的类型就都可以被外部模块访问。如果一个Java类型所在的包没有暴露,那么外部其他模块是无法import它的,即使这个类型是public的。

JPMS具有两个重要的目标,要牢记:

  • 强封装Strong encapsulation:由于每一个模块都声明了哪些包是公开的哪些包是内部的,java编译和运行时就可以实施这些规则来确保外部模块无法使用内部类型。
  • 可靠配置Reliable configuration:由于每一模块都声明了哪些是它所需的,那么在运行时就可以检查它所需的所有模块在应用启动运行前是否都有。

    除了上面两个核心的目标,JPMS还有另外一个重要的目标是易于扩展和使用即使在庞大的类库上。
    所以对Java平台自身进行了模块化,实施于项目Project Jigsaw

1.5 Project Jigsaw

Modular development starts with a modular platform. —Alan Bateman 2016.9
模块化开发始于模块化的平台。
要写模块化代码,需要将Java平台模块化。Java9之前,JDK中所有的类都糅杂在一起,像一碗意大利面。这使得JDK代码库很难改变和发展。
Java 9 模块化JDK如下图:
这里写图片描述
Project Jigsaw 有如下几个目标:

  • 可伸缩平台Scalable platform:逐渐从一个庞大的运行时平台到有有能力缩小到更小的计算机设备。
  • 安全性和可维护性Security and maintainability:更好的组织了平台代码使得更好维护。隐藏内部APIs和更明确的接口定义提升了平台的安全性。
  • 提升应用程序性能Improved application performance:只有必须的运行时runtimes的更小的平台可以带来更快的性能。
  • 更简单的开发体验Easier developer experience:模块系统与模块平台的结合使得开发者更容易构建应用和库。

模块化的另外一个重要的方面是版本控制versioning。现在的JPMS不支持versioning!!!

在控制台输入如下命令可以查看所有的模块:java –list-modules
这里写图片描述

查看某个模块(例如java.sql)的详情(描述符)使用–describe-module或-d:
java –describe-module java.sql

$ java -d java.sql
java.sql@9
exports java.sql
exports javax.sql
exports javax.transaction.xa
requires java.base mandated
requires java.logging transitive
requires java.xml transitive
uses java.sql.Driver
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

从Java平台的模块描述符中可以看出有几个关键词requires(输入)、exports(输出)、users(使用服务:消费者)、providers(提供服务:服务实现者)、transtive(传递性)
java.se 是Java SE包含的所有模块:
这里写图片描述

java.base是最基础的模块,也是java开发所需要的最小的模块,没有它就写不了java代码。因此该模块会自动隐含地加入所有模块(即所有模块描述符会隐含这条语句requires java.base),所以模块描述符中不需要明确的requires。
java.xml 是与xml有关的类型的模块。
……
从用–list-modules命令查看所有的模块的前缀中(例如java、javafx、jdk)可以看出一些规律:

  • java :指核心的Java平台模块。即官方的标准模块。
  • javafx : 指Java FX模块,即用于构建桌面应用的平台模块。
  • jdk:指核心的JDK模块。这些不是Java语言规范的一部分,但包含了一些有价值的工具和APIs。
  • oracle:如果下载的是Oracle Open JDK,就可以看到一些已oracle为前缀的模块。不建议使用。
以java.为前缀的模块又可以分为3大类:
  • 核心Java模块:指核心的Java SE APIs,例如java.bas,java.xml。
  • 企业级模块:包含一些如java.corba(包含遗留CORBA技术)和java.transaction(提供数据库事务APIs)。注意它与Java EE不同,Java EE是一个完全不同的规范。Jave SE和Java EE有一些重叠的地方,为了避免这些重叠,在Java9中已经将这些企业级模块标记为废弃,在将来的版本中可能会被移除掉。
  • 聚合(Aggregator)模块:这些模块本身没有包含任何API,而是作为一种非常简便的方式将多个模块绑在一起。目前平台有两个聚合模块java.se以及java.se.ee(JavaSe加上与JavaEE重叠的部分)。聚合模块一般是将核心的模块组合在一起使用,要小心使用。

2.构建第一个Java模块Module

首先,需要下载和安装Java 9 SDK,即JDK 9,下载链接在文章开头已经给出,推荐第一个链接。
为了验证安装和配置是否正确,打开命令行窗口,输入java -versionecho %JAVA_HOME%命令。
下面用任意的一个文本编辑器开发第一个模块应用,暂时先不用IDE。

2.1创建一个模块

  1. 给模块取个名字:例如com.acme.stringutil
  2. 创建一个模块根文件夹:根文件夹的名称和模块名一样为com.acme.stringutil
  3. 添加模块代码:如果模块有一个类StringUtil.java位于com.acme.util包下面,那么文件夹的结构则如下图所示:
    这里写图片描述

完整的目录结构如下:
这里写图片描述
4.创建和配置模块描述符:每一个模块都有一个文件用于描述这个模块包含的元数据。这个文件叫做模块描述符module descriptor。这个文件包含了这个模块的信息,如输入输出。通常这个文件直接位于模块的根文件夹下,通常取名为 module-info.java.下面是这个文件的最小的配置内容:

module com.acme.stringutil {
}
  • 1
  • 2

注意该描述符中虽然没有任何内容,但是隐含的requries java.base模块。
这里写图片描述

2.2 创建第一个模块

通常一个应用会包含很多个模块,先创建一个模块叫packt.addressbook。
接下来需要创建一个Java文件叫Main.java,放置在packt.addressbook包中,其完整路径为:
~/code/java9/src/packt.addressbook/packt/addressbook/Main.java
Main.java内容如下:

package packt.addressbook;
public class Main{
    public static void main(String[] args){
        System.out.println("Hello World!");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

最后创建一个模块描述符文件module-info.java,将它直接放在模块根目录下。到此就完成了:
这里写图片描述

2.3 编译模块

编译模块需要用到javac命令。(确保JAVE_HOME和path已经配置好了)goto 到项目根目录下 ~/code/java9 输入命令:

javac --module-source-path src -d out src/packt.addressbook/packt/addressbook/Main.java src/packt.addressbook/module-info.java
  • 1

这里写图片描述

当编译成功后,控制台没有输入。out目录应该包含编译好的类:
这里写图片描述

2.4 执行模块

执行上一步编译好的代码,需要在相同的目录~/code/java9下运行如下命令:
java --module-path out --module packt.addressbook/packt.addressbook.Main

这里写图片描述

–module-path可以用-p代替,–module可以用-m代替
如果执行成功,可以在控制台看到Hello World!

2.5 创建第二个模块

为了示例多个模块,将上面的应用分解为两个模块。
接下来创建第二个模块,然后让上面第一个模块使用第二个模块。

  • 创建一个新的模块,命名为: packt.sortutil.
  • 将排序相关的代码移到新的模块packt.sortutil中。
  • 配置packt.sortutil模块描述符(输入输出是什么)。
  • 配置 packt.addressbook 依赖新模块packt.sortutil。

    下面是文件夹结构:
    这里写图片描述

这里写图片描述

其中packt.sortutil的模块描述符module-info.java:

module packt.sortutil {
    exports packt.util;
}
  • 1
  • 2
  • 3

packt.addressbook的模块描述符module-info.java:

module packt.addressbook {
    requires packt.sortutil;
}
  • 1
  • 2
  • 3

这样packt.sortutil模块可以当做库来使用了,但需要注意到的是,当你创建了一个库,在你允许其他人使用它时,你要非常谨慎地定义你的库的API。原因是一旦其他人开始使用你的库,就很难去对库的public API做改变。将来版本的对API的任何改变都意味着需要你的库的所有使用者要更新他们的代码来时新的API有效。

所以尽量使SortUtil类轻量点,让它作为packt.sortutil库的接口。所以可以将实际的排序逻辑放到一个实现类中,例如创建一个实现类BubbleSortUtilImpl.java:

public class BubbleSortUtilImpl {
    public <T extends Comparable> List<T> sortList(List<T> list) {
        ...
    }
    private <T> void swap(List<T>list, int inner) {
    ...
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然后SortUtil.java类可以很简单的代理执行排序方法:

public class SortUtil {
    private BubbleSortUtilImpl sortImpl = new BubbleSortUtilImpl();
    public <T extends Comparable> List<T> sortList(List<T> list) {
        return this.sortImpl.sortList(list);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

模块结构如下:
这里写图片描述
由于实现类BubbleSortUtilImpl.java放到一个新的包,因此对外是隐藏的,也就是说外部模块是无法直接使用BubbleSortUtilImpl类的。这是不是解决了Java9之前无法隐藏内部类型的问题了呢?
注意java中的包是没有递阶控制的not hierarchical。包packt.util和包packt.util.impl是两个独立的包,二者毫无关系。暴露packt.util包并没有将packt.util.impl暴露。
这里写图片描述

编译:javac -d out –module-source-path src –module packt.addressbook,packt.sortutil
执行:java –module-path out -m packt.addressbook/packt.addressbook.Main

3.模块概念Module Resolution, Readability, and Accessibility

Java9模块化有3个概念非常重要,模块解析、可读性、可访问性。

3.1 Readability 可读性

当一个模块依赖另一个模块时,即第一个模块read读第二个模块。也就是说第二个模块可以被第一个模块读readable。用图表示就是第一个模块箭头指向第二个模块。假设有下面3个模块,关系如下:
这里写图片描述
模块A requiers B。因此模块A reads B。模块B is readable by A。同理模块C reads B。
然而模块A does not read C ,反之亦然。

你会发现上面的关系是非对称的。事实上,在Java模块系统中,是可以保证模块间的关系绝对是非对称asymmetric的。why?因为如果两个模块可以互相read,那么它们会形成一个循环依赖,这是平台所不允许的。所以一个模块requires第二个模块,那第二个模块必定不能requires第一个模块。
一个特殊的模块是java.base,每一个模块首先都会read它。这个依赖是自动完成的而且并不需要显示地requires。

可读性readability关系是最基础的,它实现了Java模块系统两个主要目标之一,即可靠性配置reliable configuration。

3.2 Accessibility 可得性

Accessibility 是Java模块的另一性质。如果可读性readability关系表明了某个模块可以读read哪些模块,那么accessibility表明这个模块可以实际从中读取什么。一个模块被其他模块read时,中并不是所有的东西都可以accessible,只有那些使用export标记的包中的public类型才可以被访问到。

所以,对于模块B中的一个类型type(可以是interface、class等)能够被模块A访问到,需要同时满足一下几个条件:

  • 模块A需要 read B
  • 模块B需要暴露export包含了该类型的包
  • 该类型本身是public的
3.2.1 接口实现accessibility

我们可以考虑在LibApi接口中添加一个static方法来创建一个LibApiImpl类的实例。它的返回类型是LibApi,这一点很重要。

package packt.lib.external;
public interface LibApi {
    static LibApi createInstance() {
        return new LibApiImpl();
    }
    public void testMethod();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

然后构建一个简单的实现类LibApiImpl实现LibApi接口,注意在类前没有关键字public修饰符。这意味着这个类是包所有package-private,不是public。即使它与LibApi在同一个包中被模块暴露export,它仍然是不可被外部模块访问到的。

package packt.lib.external;
class LibApiImpl implements LibApi {
    public void testMethod() {
        System.out.println("Test method executed");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

外部模块就可以这样使用它:

package packt.app;
import packt.lib.external.LibApi;
public class App {
    public static void main(String[] args) {
        LibApi api = LibApi.createInstance();
        api.testMethod();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这种设计模式是非常有价值的,因为它可以让你在重写底层的实现时不用改变提供的public APIs。当然让实现类放到另一个包中不让它暴露也可实现同样的效果。

3.2.2 Split packages 分离包

上面外部模块中的包packt.app中的类App无法直接访问到包packt.lib.external中的LibApiImpl类,你可能会想如果类App放在外部模块的一个相同名的包packt.lib.external中,那是否可以访问LibApiImpl呢?这当然行不通,在编译时就发生错误:package exists in another module
是的!同一包名不能同时存在于两个模块中。至少不能存在于两个可观察到observable的模块中。换句话说,一个应用的某个包,在模块路径上它只能是唯一地属于某个模块。

传统的类路径上的多个Jar文件是可以同时包含相同的包的。而模块是不允许共享包(即Split packages 分离包)。

3.3 Implied readability 隐含的可读性

下面来看一个依赖泄露的问题。假设有3个模块依赖关系如下:
这里写图片描述

module A {
    requires B;
} 
module B {
    requires C;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

模块A requires B,模块B requires C 。目前为止我们知道A does not read C ,因为本质上模块依赖性非传递性 not transitive的。但万一我们需要呢?例如模块B有一个API,它的返回类型是模块C中的。

有一个好的例子可以从Java平台自身中找到。比如你自己的模块依赖了java.sql模块。你就可以使用该模块里面的Driver接口了。这个Driver接口有一个方法叫getParentLogger(),它返回Logger类型,改类型在java.logging模块当中。定义如下:

Logger getParentLogger() throws SQLFeatureNotSupportedException
  • 1

下面是你自己的模块调用的代码:

Logger myLogger = driver.getParentLogger();
  • 1

你在你的模块描述符中只添加requires java.sql语句,这样就可以了吗?下面看下依赖关系:
这里写图片描述
由于你自己的模块并没有直接require java.logging,为了使用java.sql模块的API,你还得require java.logging 模块!
那有没有更好的方式呢?尽管默认下依赖不具有传递性,但有时我们想可以有选择的让某些依赖具有传递性。Java9有这么个关键词transitive(传递性)可以做到。使用方式requires transitive <module-name>;

所以之前模块A要可以访问到C,可以这么做:

module A {
    requires B;
} 
module B {
    requires transitive C;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

现在模块C不仅仅可以被B可读,而且所有依赖B的模块都可以读C。这样A可以读C了。
模块A没有直接requires C,但通过transitive,可以读到C,这种关系就是隐含的可读性

在命令行运行:java -d java.sql

$ java -d java.sql
module java.sql@9
exports java.sql
exports javax.sql
exports javax.transaction.xa
requires transitive java.logging
requires transitive java.xml
requires mandated java.base
uses java.sql.Driver
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注意到有两个模块java.logging和java.xml都用transitive做了标记。这就意味着那些依赖于java.sql的模块将自动地可以访问java.logging和java.xml。这是Java平台做出的决定,因为使用java.sql的APIs时也需要使用到其它两个模块。因此之前自己模块只依赖java.sql是没有问题的,因为可以隐含的read模块java.logging。
这里写图片描述

在你的模块中添加传递性transitive 依赖需要十分谨慎。想象下你只依赖的一个模块但由于其使用了transitive 你却无心地得到了几十个其他的模块依赖。这种模块设计显然是违背了模块化的原则。所以除非万不得已,千万不要使用传递性transitive 。

然而,实际上有一个非常有趣和简便的使用传递性依赖的方式,那就是聚合模块 aggregator modules。
命令行运行:java -d java.se

$ java -d java.se
java.se@9
requires java.scripting transitive
requires java.xml transitive
requires java.management.rmi transitive
requires java.logging transitive
requires java.sql transitive
requires java.base mandated
...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我希望你可以拒绝使用聚合模块的诱惑。你可以使用java.se聚合模块,但这样你就失去了模块化的目的又回到了Java8及以前的模式(通过依赖整个平台APIs而不管你实际需要的其中的哪部分)。这个聚合模块主要是用于遗留代码的迁移的作用。
java.se.ee聚合模块已经废弃了并不赞成使用。

3.4 Qualified exports 限定输出

上一节介绍了传递性依赖如何对readability可读性关系稍作了调整,这一小节将介绍一种对accessibility关系稍作调整的方式。通过使用qualified exports
考虑一下这样一种需求:假如模块B被A使用,那模块B中的暴露的public类型就可以被A使用了,但B中某个私有包(没有暴露)仅仅可以被模块A使用,而不能被其他模块所使用,那该怎么做?
可以使用限定输出:exports <package-name> to <module1>, <module2>,... ;

module B {
    exports moduleb.public; // Public access to every module that reads me
    exports moduleb.privateA to A; // Exported only to module A
    exports moduleb.privateC to C; // Exported only to module C
}
  • 1
  • 2
  • 3
  • 4
  • 5

命令行运行 java -d java.base

module java.base@9
...
exports jdk.internal.ref to java.desktop, javafx.media
exports jdk.internal.math to java.desktop
exports sun.net.ext to jdk.net
exports jdk.internal.loader to java.desktop, java.logging,
java.instrument, jdk.jlink
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

要记住使用限定输出通常是不推荐的。模块化原则的推荐是一个模块不应该被使用者所感知到。限定输出在一定程度上增加了两个模块的耦合度。除非万不得已,不要使用限定输出。

3.5 Services 服务

紧耦合tight coupling是指两个实体高度依赖彼此以至于改变其中某个的行为时,需要调整实际的其中一个甚至二者的代码。松耦合 loose coupling则与之相反,两个实体没有高度依赖,它们之间甚至不知道彼此的存在,但二者仍然可以互相交互。
那在Java模块系统中两个模块的耦合是紧耦合还是松耦合呢?答案明显是紧耦合。
来举个例子。现在我们有一个排序的模块叫 packt.sortutil。我们通过配置让这个模块暴露了一个接口以及封装了一个实现。它只有一个实现,所有其他模块能做的只是bubble排序。如果我们需要有多个排序模块,让消费者consumer模块自己选择其中的一个模块去使用呢?
这里写图片描述

我们可以添加多个模块提供不同的排序实现。但是,由于紧耦合,需要consumer模块packt.addressbook不得不对每一个排序模块都requires,尽管任何时候它可能只会使用到其中的一个。有没有一种接口作为只让消费者consumer模块来依赖它就可以呢?有的!那就是Services!

这里写图片描述

Java开发者应该很熟悉一个概念–多态polymorphism。它从一个接口及其多个实现开始。让我们定义一个服务接口叫MyServiceInterface.java:

package service.api;
public interface MyServiceInterface {
    public void runService();
}
  • 1
  • 2
  • 3
  • 4

考虑到有3个接口的实现分别在不同的模块,它们都要访问到MyServiceInterface接口来实现。MyServiceInterface 接口在模块service.api中,如下图所示:
这里写图片描述
现在consumer消费者模块需要调用这些实现中的一个来运行服务。为了达到这个目标,不能让消费者模块直接read这些实现,因为这是紧耦合的。我们让消费者模块只允许read接口模块service.api.

3.5.1 The service registry 服务注册表

为了跨过消费者模块与实现者之间没有紧耦合的桥,想象在二者直接有一个层叫做the service registry服务注册表。服务注册表是由模块系统提供的一个层,用于记录和注册给定接口的实现作为服务。当消费者需要一个实现时,它就使用服务API来与服务注册表交流,并获得可用实现的实例。这样就打破了provider和consumer的耦合度。接口是其他模块所共享的公用实体。由于provider和consumer之间完全无法感知彼此的存在,所以你可以任意的移除的其中的一个实现或者加入一个实现。那么模块是如何注册登记register它们的实现呢?消费者模块又是如何从注册表registry中访问实例呢?下面来看实现的细节。

3.5.2 Creating and using services创建和使用服务

1.创建Java类型来定义服务:每一个服务可以是一个简单的Java类型,它可以是接口、抽象类甚至是常规的类。让接口作为服务通常比较理想。定义创建一个模块,并在其中创建一个包含了该接口的包,并暴露它。例如模块service.api 包含接口service.api.MyServiceInterface。

module service.api {
    exports service.api;
}
  • 1
  • 2
  • 3

2.创建一个或多个模块都read接口模块并实现接口。
3.让实现模块注册它们自己作为服务提供者service providers:语法如下provides <interface-type> with <implementation-type>;
例如:模块service.implA 的实现类实现了MyServiceInterface接口,模块描述符如下

module service.implA {
    requires service.api;
    provides service.api.MyServiceInterface with
    packt.service.impla.MyServiceImplA;
}
  • 1
  • 2
  • 3
  • 4
  • 5

4.让消费者模块注册自己作为服务的一个消费者:使用关键词users,语法是,uses <interface-type>;
消费者模块描述符:

module consumer {
    requires service.api;
    uses service.api.MyServiceInterface;
}
  • 1
  • 2
  • 3
  • 4

5.在消费者模块中调用ServiceLoader API来访问提供者实例:由于没有直接依赖,服务实现者完全无法感知到消费者。因此无法实现new来实例化它。为了可以访问到所有已经注册实现的提供者,你需要在消费者模块中调用Java平台APIServiceLoader.load() 方法。

Iterable<MyServiceInterface> sortUtils =
ServiceLoader.load(MyServiceInterface.class);
  • 1
  • 2

这里是依赖查询dependency lookup,区分下Spring框架的依赖注入dependency injection。
上面只是得到一个Iterable,显然实际应用是需要一个选择策略来选择其中某个实现。例如排序接口SortUtil 可以定义一个根据集合大小来选择哪个实现。

public interface SortUtil {
    public <T extends Comparable> List<T> sortList(List<T> list);
    public int getIdealMaxInputLength();
}
  • 1
  • 2
  • 3
  • 4

那么它的实现者都以实现这个getIdealMaxInputLength接口,比如返回4或者Integer.MAX_VALUE等等。
为了方便消费者使用,可以把选择策略的逻辑放到排序接口SortUtil 中,可以利用接口静态方法实现:

public interface SortUtil {
    public <T extends Comparable> List<T> sortList(List<T> list);
    public int getIdealMaxInputLength();
    public static Iterable<SortUtil> getAllProviders() {
        return ServiceLoader.load(SortUtil.class);
    } 
    public static SortUtil getProviderInstance(int listSize) {
        Iterable<SortUtil> sortUtils =ServiceLoader.load(SortUtil.class);
        for (SortUtil sortUtil : sortUtils) {
            if (listSize < sortUtil.getIdealMaxInputLength()) {
                return sortUtil;
            }
        }
        return null;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

现在消费者模块的Main方法就不用和ServiceLoader交互和循环查询实现者实例:

SortUtil sortUtil = SortUtil.getProviderInstance(contacts.size());
sortUtil.sortList(contacts);
  • 1
  • 2

3.6 Understanding Linking and Using jlink

到目前为止,已经介绍了模块化的几个重要概念包括readability 、accessibility以及强大的services服务。这一小节将介绍应用开发的最后一个步骤–构建和打包应用。

3.6.1 Module resolution process 模块解析过程

Java9之前,Java编译器和Java运行时runtime会去寻找用于组成类路径classpath的一些文件夹和JAR文件。这个类路径是你可以在编译阶段可以传给编译器也可以在执行阶段传给运行时的配置选项。

而模块则不同。我们不必再使用通常的类路径了。由于每一个模块都定义了它的输入和输出。现在就可以明确知道哪部分代码是所需的。如下图:
这里写图片描述
假设你要执行模块C中的main方法,这最小的集合显然是CBDA,而E是不需要的。而如果要执行E中的main方法,这次最小集合仅仅是ED,其他模块可以忽略。为了得到哪些模块是必需哪些是非必需的, 平台会运行一个过程来解析模块,这个过程就叫做模块解析过程module resolution process

在图论中,这个过程是指发现传递闭包,称为有向无环图directed acyclic graph 。
有一个命令行选项可以用来查看模块解析过程:–show-module-resolution

3.6.2 Linking using jlink

JDK9捆绑了一个新的工具叫 jlink,它可以让你构建你自己的完整的运行时镜像来运行你的应用。
jlink命令需要3个输入,

  • The module path:已经编译好的模块所在的路径。多个路径之间windows用分号分隔(Mac或Linux用冒号分隔)
  • The starting module:解析过程从哪个模块开始。可以是多个,用逗号分隔。
  • The output directory:存放生成的镜像的目录。

语法如下:

jlink --module-path <module-path-locations>
--add-modules <starting-module-name>
--output <output_location>
  • 1
  • 2
  • 3

记住模块解析过程只会识别requires语句。而服务Services是默认不会包含进去的,需要明确地加到–add-modules选项后面。另外一种简便的方式是可以使用–bind-services选项。

这个链接步骤是可选的,它位于编译阶段和执行阶段的中间。但是如果你将要使用jlink,你就有机会去做些优化,例如压缩镜像,确定和移除未使用到的类型等等。

3.6.3 Building a modular JAR file 构建一个模块JAR文件

通过使用jar命令:

$ jar --create --file out/contact.jar --module-version=1.0
-C out/packt.contact
  • 1
  • 2

甚至有main方法的模块也可以转换成Jar文件,例如:

$ jar --create --file out/addressbook-ui.jar --module-version=1.0
--main-class=packt.addressbook.ui.Main -C out/packt.addressbook.ui 
  • 1
  • 2

这样可以直接使用java命令运行它。

4. 其他

  • Optional dependencies 可选依赖:语法格式为,requires static <optional-module-dependency>;限定符static告诉模块系统跟在其后的模块是可选的optional(运行时),也就是说在运行时,如果该模块不可用,会出现一个NoClassDefFound错误,通常需要捕获它。
  • Optional dependencies using services 使用服务的可选依赖 : 将原来服务接口放到消费者中(不需要服务接口模块),让服务实现者依赖消费者模块。这样消费之模块是无法感知服务提供者,服务提供者是可选的Optional 消费者可以自己实现默认接口。
  • Open modules for reflection 用于反射的开放模块:现在由于模块的强封装性,所有封装的类型是无法通过放射获取到的,像用户自定义的类型,那用到了反射的框架如Spring现在该如何扫描类型呢?为了解决这个问题,平台引入了一个概念叫开放模块open modules.要让整个模块都open,只需要在module关键字前面加上open关键字。例如:open module <module-name> {}。这样模块内容还是封装的,但是在运行时它可以用反射所访问到。当然也可以只对模块中的某些包open,甚至可以让某个包只能被某个模块访问,例如:
    module modulename {
    opens package.one;
    opens package.two to anothermodule;
    exports package.three;
    }

5.开发工具IDE

目前支持JDK9的开发工具有NetBeans和IntelliJ Idea,Elcipse尚在开发中,推荐使用NetBeans。注意如果官网的地址NetBeans官网发布地址 不支持Java 9,那么可以到下面地址下载开发版本NetBeans开发版本地址
.
这里写图片描述
目前感觉Java8及之前的项目要想迁移到Java9有点麻烦,毕竟许多第三方的Jar包还没有模块化。所以在此不具体介绍代码迁移。

相关下载链接:
Modular Programming in Java 9_英文版.pdf
Java9模块化编程_附带源码.zip

《道德经》第一章:
道可道,非常道。名可名,非常名。无名天地之始。有名万物之母。故常无欲以观其妙。常有欲以观其徼。此两者同出而异名,同谓之玄。玄之又玄,众妙之门。

译文:“道”如果可以用言语来表述,那它就是常“道”(“道”是可以用言语来表述的,它并非一般的“道”);“名”如果可以用文辞去命名,那它就是常“名”(“名”也是可以说明的,它并非普通的“名”)。“无”可以用来表述天地浑沌未开之际的状况;而“有”,则是宇宙万物产生之本原的命名。因此,要常从“无”中去观察领悟“道”的奥妙;要常从“有”中去观察体会“道”的端倪。无与有这两者,来源相同而名称相异,都可以称之为玄妙、深远。它不是一般的玄妙、深奥,而是玄妙又玄妙、深远又深远,是宇宙天地万物之奥妙的总门(从“有名”的奥妙到达无形的奥妙,“道”是洞悉一切奥妙变化的门径)。

原文地址:https://www.cnblogs.com/xiang--liu/p/9710339.html