提示

本文主要讲解 Java 9 的模块系统。@ermo

# Java 9 模块系统

Java9 模块系统全称为 Java Platform Module System(JPMS)。模块是一组紧密联系的包和资源,也可以理解为 Java 概念中包(Package)的集合。

在 Java9 之前,项目的最高层结构就是包,包内的资源只能通过访问修饰符控制外部的访问权限。因为反射这一概念,包的概念并不能为 Java 程序提供足够的安全性。

这时候模块的优势就体现出来了,有了模块系统,除了可以更好的管理 Java 资源文件(可维护性)之外,还使得 Java 程序更加可靠,有效阻止内部 API 被外部访问。

# 第一个单模块项目(quick start)

为了让读者快速了解模块系统,本文先创建一个简单的单模块项目,然后编译并运行起来。

# 环境变量

创建项目前,需要设置 Java 环境变量。可以参考 mac设置环境变量

运行本文项目需要至少需要 java9 或者更高的 JDK 版本。

# 创建项目

参考下面命令,创建基本的目录结构。

cd ~
mkdir cc-ermo-java9-module
mkdir -p cc-ermo-java9-module/cc.ermo.hello/src/main/java/cc/ermo/hello

创建 module-info.java 文件,该文件记录了模块的属性和依赖关系。

vi cc-ermo-java9-module/cc.ermo.hello/src/main/java/module-info.java

添加下面内容。

module cc.ermo.hello {}

创建 Main.java 文件,并添加程序入口 main 方法。

vi cc-ermo-java9-module/cc.ermo.hello/src/main/java/cc/ermo/hello/Main.java

Main.java 的内容为。

package cc.ermo.hello;

public class Main {
    public static void main(String[] args) {
        System.out.println("Hello World.");
    }
}

# 编译项目

进入到当前项目内,预先创建好编译目录。

cd cc-ermo-java9-module
mkdir -p outDir/cc.ermo.hello

使用 javac 编译项目。

javac -d outDir/cc.ermo.hello \
          cc.ermo.hello/src/main/java/module-info.java \
          cc.ermo.hello/src/main/java/cc/ermo/hello/Main.java

如果命令行没有报错的话,在 outDir 目录下会生成对应的字节码文件。

这里解释下命令行中的参数:

  • -d outDir/cc.ermo.hello 代表项目编译后的输出目录
  • cc.ermo.hello/src/main/java/module-info.java 指定模块描述符文件
  • cc.ermo.hello/src/main/java/cc/ermo/hello/Main.java 指定主程序入口

# 运行项目

执行 java 命令运行项目。

cd cc-ermo-java9-module
java --module-path outDir -m cc.ermo.hello/cc.ermo.hello.Main

执行成功后可以看到输出。

Hello World.

到这里,整个 cc-ermo-java9-module 目录下的文件结构应该是这样的。

|-- cc-ermo-java9-module
    |-- cc.ermo.hello
    |   |-- src
    |       |-- main
    |           |-- java
    |               |-- module-info.java
    |               |-- cc
    |                   |-- ermo
    |                       |-- hello
    |                           |-- Main.java
    |-- outDir
        |-- cc.ermo.hello
            |-- module-info.class
            |-- cc
                |-- ermo
                    |-- hello
                        |-- Main.class

# 模块描述文件 module-info.java

# 模块的命名

上面例子中创建了一个名称为 cc.ermo.hello 的目录,并且在 src/main/java 目录下有一个 module-info.java 文件。这个文件就代表着一个模块的说明书,是对当前模块的一个解释。

注意 module-info.java 文件一定要放到源代码的根目录下,当前的源代码根目录是 project/cc.ermo.hello/src/main/java

module-info.java 的内容如下。

module cc.ermo.hello {

}

module 作为 java 9 模块的关键字,用于声明一个模块。cc.ermo.hello 是当前模块的名字,命名规则如下:

  • 模块名称命名规则和包名类似,推荐使用域名倒叙排列,使用 . 隔开
  • 模块命名只能用大小写英文、数字、下划线 _ 和美元符号 $,其中数字不能作为开头
  • 模块命名要和当前模块的根目录名称相同,推荐这么做,这样从模块目录名称就可以推断出模块的名称以及主要作用

# 模块导出包 exports

一个工业级别的 Java 项目通常由多个模块组成,每个模块都充当着消费者(Consumer)和提供者(Provider)的角色。

默认情况下一个模块的资源外部模块是无法访问到的,这就是模块和包最大的区别。如果想要公开一个或多个包的资源供外部模块使用,需要使用到 exprots 指令,语法如下。

module 模块名 {
    exports 包名;
    exports 包名;
    // ...
}

比如 cc.ermo.provider 模块想要将包 cc.ermo.provider.util 包开放出来供其他模块使用,就可以这么写。

module cc.ermo.provider {
    exports cc.ermo.provider.util;
}

也可以使用 exports ... to ... 指令,将当前包指定公开给其他包使用。

module cc.ermo.provider {
    exports cc.ermo.provider.util to cc.ermo.hello;
}

上述例子就表示包 cc.ermo.provider.util 下的 public 资源只能给包 cc.ermo.hello 内的资源使用。

# 模块依赖列表 requires

如果当前模块需要依赖到某一个模块或者某几个模块,就需要使用到 requires 关键字,语法如下。

module 模块名 {
    requires 依赖模块名;
    requires 依赖模块名;
}

比如当前模块 cc.ermo.hello 要依赖到 cc.ermo.provider 模块,那么 module-info.java 的内容应该这样写。

module cc.ermo.hello {
    requires cc.ermo.provider;
}

表示在 cc.ermo.hello 模块中可以使用 cc.ermo.provider 模块中所有导出(exports)的 public 类型的资源。

有时候我们只需要在编译时使用到一些模块,例如测试过程,这时候要使用到 requires static 指令,语法如下。

module 模块名 {
    requires static 依赖模块名;
}

还有一种情况,模块 a 要依赖到模块 b,模块 b 要依赖模块 c,如果模块 a 要用到模块 c 的话,通常情况我们要这么写。

module a {
    requires b;
    requires c;
}

module b {
    exports b.package;
    requires c;
}

module c {
    exports c.package;
}

实际开发中模块依赖的深度是很深的,这样会有很多重复的依赖代码,这时候要借助模块依赖传递指令 requires transitive 指令。

这样模块 a 就可以使用到模块 c 的公开资源了,我们对上面代码进行改造。

module a {
    requires transitive b;
}

module b {
    exports b.package;
    requires c;
}

module c {
    exports c.package;
}

# 使用服务 uses

可以使用 uses 指令来明确指定需要的服务,这个服务一般是接口或者抽象类,语法如下。

module 模块名 {
    uses 接口全限定类名;
}

usesrequires 的区别是,后者的范围更广,可以依赖到整个模块的公开导出(exports)资源,而 uses 更加明确,可以避免依赖到无用的依赖模块。

比如模块 cc.ermo.hello 要使用到 cc.ermo.provider 中的 HelloInterface 接口,就可以这样写。

module cc.ermo.hello {
    uses cc.ermo.provider.HelloInterface;
}

# 提供服务 provides... with

provides... with 指令可以提供接口服务供其他模块使用,语法如下。

module 模块名 {
    provides 接口全限定类名 with 接口实现类全限定类名;
}

比如模块 cc.ermo.provider 要提供一个 HelloInterface 接口服务,就可以这样写。

module cc.ermo.provider {
    provides cc.ermo.provider.HelloInterface with cc.ermo.provider.HelloInterfaceImpl;
}

上例中 cc.ermo.provider.HelloInterfaceImplHelloInterface 的具体实现。

# 开放反射 open

java 9 之前,任何包下的资源都可以通过反射调用。如果我们限制某个模块开放反射权限,可以使用 open 指令,语法如下。

open module 模块名 {

}

想要将一个模块下单一个或多个包开发反射权限,可以使用 opens 指令,语法如下。

module 模块名 {
    opens 包名;
    opens 包名;
    // ...
}

也可以将一个模块下的几个包的反射权限开发给指定的一个或多个模块,语法如下。

module 模块名 {
    opens 包名 to 模块1,模块2,...,模块n;
}