参考文档:JDK9的新特性:JPMS模块化

1. 简介

JDK9 引入了一个新的特性叫做 JPMS(Java Platform Module System),也可以叫做 Project Jigsaw。模块化的本质就是将一个大型的项目拆分成为一个一个的模块,每个模块都是独立的单元,并且不同的模块之间可以互相引用和调用。

在 module 中会有元数据来描述该模块的信息和该模块与其他模块之间的关系。这些模块组合起来,构成了最后的运行程序。

听起来是不是和 gradle 或者 maven 中的模块很像?

通过组件化,我们可以根据功能来区分具体的模块,从而保持模块内的高聚合,模块之间的低耦合。

另外,我们可以通过模块化来隐藏接口的具体实现内容,从而不影响模块之间的调用。

最后,我们可以通过显示声明来描述模块之间的依赖关系。从而让开发者更好的理解系统的应用逻辑。

2. 模块原理

在 JDK9 之前,java 是通过不同的 package 和 jar 来做功能的区分和隔离的。

但在 JDK9 中,一切都发送了变化。

首先是 JDK9 本身的变化,JDK 现在是以不同的模块来区分的,如果你展开 IDE 中 JDK 的依赖,会看到 java.base,java.compiler 等模块。

img

其中 java.base 模块比较特殊,它是独立的模块,这就意味着它并不依赖于其他任何模块,并且 java.base 是其他模块的基础,所以在其他模块中并不需要显式引用 java.base。

我们再总结一下:

class 是字段和方法的集合,package 是 class 的集合,而 module 是 package 的集合。

一般来说使用模块和不使用模块对用户来说基本上是感觉不到的,因为你可以将模块的jar包当成普通的jar包来使用,也可以将普通的jar包当成模块的jar包来使用。

当使用普通的 jar 包时,JDK 将会采用一种 Automatic modules 的策略将普通 jar 包当成 module jar 包来看待

那么 module 和普通的 jar 包有什么区别呢?

  1. module 中包含有一个 module-info.class 文件,这个文件定义了 module 本身的信息和跟外部 module 之间的关系。
  2. 要使用 module jar 包,需要将该 jar 包放入 modulepath 而不是 classpath。

3. 实例

假如我们有一个 controller,一个 service 的接口,和两个 service 的实现。

为了简单起见,我们将这四个类分在不同的 module 中。

在 IDEA 中创建一个 module 很简单,只需要在 java 文件夹中添加 module-info.java 文件就可以了。如下图所示:

img

代码其实很简单,Controller 引用了 Service 接口,而两个 Service 的实现又实现了 Service 接口。

看下 controller 和 service 两个模块的的 module-info 文件:

module com.flydean.controller {
    requires com.flydean.service;
    requires lombok;
}
module com.flydean.service {
    exports com.flydean.service;
}

requires 表示该模块所要用到的模块名字。exports 表示该模块暴露的模块中的包名。如果模块不暴露出去,其他模块是无法引用这些包的。

看下在命令行怎么编译,打包和运行 module:

$ javac
    --module-path mods
    -d classes/com.flydean.controller
    ${source-files}
$ jar --create
    --file mods/com.flydean.controller.jar
    --main-class com.flydean.controller.ModuleController.Main
    -C classes/com.flydean.controller .
$ java
    --module-path mods
    --module com.flydean.controller

4. module-info 用法

4.1 transitive

先看下 modulea 的代码:

public ModuleService getModuleService(){
	return new ModuleServiceA();
}

getModuleService 方法返回了一个 ModuleService,这个 ModuleService 属于模块 com.flydean.service,我们看下 module-info 的定义:

module com.flydean.servicea {
    requires com.flydean.service;
    exports com.flydean.servicea;
}

看起来好像没什么问题,但是如果有其他的模块来使用 servicea 的 getModuleService 方法就会有问题,因为该方法返回了模块 com.flydean.service 中定义的类。所以这里我们需要用到 transitive。

module com.flydean.servicea {
    requires transitive com.flydean.service;
    exports com.flydean.servicea;
}

transitive 意味着所有读取 com.flydean.servicea 的模块也可以读取 com.flydean.service。

4.2 static

有时候,我们在代码中使用到了某些类,那么编译的时候必须要包含这些类的 jar 包才能够编译通过。但是在运行的时候我们可能不会用到这些类,这样我们可以使用 static 来表示,该 module 是可选的。

比如下面的 module-info:

module com.flydean.controller {
    requires com.flydean.service;
    requires static com.flydean.serviceb;
}

4.3 exports to

在module info中,如果我们只想将包export暴露给具体的某个或者某些模块,则可以使用exports to:

module com.flydean.service {
    exports com.flydean.service to com.flydean.controller;
}

上面我们将 com.flydean.service 只暴露给了 com.flydean.controller。

4.4 open pacakge

使用 static 我们可以在运行时屏蔽模块,而使用 open 我们可以将某些 package 编译时不可以,但是运行时可用。

module com.flydean.service {
    opens com.flydean.service.subservice;
    exports com.flydean.service to com.flydean.controller, com.flydean.servicea, com.flydean.serviceb;
}

上面的例子中 com.flydean.service.subservice 是在编译时不可用的,但是在运行时可用。一般来说在用到反射的情况下会需要这样的定义。

4.5 provides with

假如我们要在 Controller中使用 service 的具体实现,比如 servicea 和 serviceb。一种方法是我们直接在 controller 模块中使用 servicea 和 serviceb,这样就高度耦合了。

在模块中,我们可以使用 provides with 来对模块之间的耦合进行解耦。

我们看下代码:

module com.flydean.controller {
    uses com.flydean.service.ModuleService;
    requires com.flydean.service;
    requires lombok;
    requires slf4j.api;
}
module com.flydean.servicea {
    requires transitive com.flydean.service;
    provides com.flydean.service.ModuleService with com.flydean.servicea.ModuleServiceA;
    exports com.flydean.servicea;
}
module com.flydean.serviceb {
    requires transitive com.flydean.service;
    provides com.flydean.service.ModuleService with com.flydean.serviceb.ModuleServiceB;
    exports com.flydean.serviceb;
}

在 controller 中,我们使用 uses 来暴露要实现的接口。而在具体的实现模块中使用 provides with 来暴露具体的实现。

怎么使用呢?我们在controller中:

public static void main(String[] args) {
    List<ModuleService> moduleServices = ServiceLoader
    .load(ModuleService.class).stream()
    .map(ServiceLoader.Provider::get)
    .collect(toList());
    log.info("{}",moduleServices);
    }

这里我们使用了 ServiceLoader 来加载接口的实现。这是一种很好的解耦方式,这样我可以将具体需要使用的模块放在 modulepath 中,实现动态的修改实现。