New Features Of JDK - JDK9 Modular System

Modular System 是 JAVA9 中提供的新特性,它从一个独立的开源项目而来,名为 Jigsaw Project。在此之前,我们对于 Java 技术栈中模块化的认知是基于 OSGI 的,实际上 OSGI 也确实形成了它自己独有的体系,并且是一定程度上的行业标准。

JAVA 模块化发展

JAVA 从没有停止过在模块化事情上的努力,比如 JSR 294 提出的 superpackages,JSR 277 中的 Java Module System(后来被 JSR 376 替换掉了);直到 Jigsaw 这个原型项目的出现,这个原本计划在 Java7 一起交付的功能,也一直被推迟到 Java9 才提供出来;作为原型项目,Jigsaw 提供了 JPMS(Java Platform Module System) 规范的参考实现。

另一个是独立于 Java 社区发展的 OGSI。到目前为止,OSGi 已经发展超过 20 年,OSGi 是应用程序模块化的事实标准。一方面 OGSI 不是 Java 平台的直接组成部分,所以它不会影响平台本身的模块化发展,另一个重要的因素则是 OSGi 使用的是类加载器实现的模块化隔离,这与 Jigsaw 基于可访问性规则实现的隔离机制完全不同。

话说回来,为什么模块化会如此的重要呢?

首先是 JAVA 自身的不断臃肿,从 JAVA 1.1 的小于 10M 到 JAVA 8 的 200M+,不管是安装占用空间还是内存要求都有相应增加,这个增加虽说是由新功能的迭代带来的,并且这些新功能中的绝大部分是受欢迎的;但是换个角度说,每一项新功能都会为不需要它的用户造成膨胀,可以肯定的是,不会有哪个工程师或者哪个团队会使用到 Java 提供的所有能力(比如你做 web 项目,还不得不带上 swing)。

另一点,也是 OSGI 能够发展的原因,依托类加载器来实现业务层面的隔离,并且具备动态载入的能力,这也使得 plugin 机制或者热加载机制能够有非常大的发挥空间。

Java 9 中的 Module System

模块化的前提是模块划分,JDK 自身也进行了模块化的处理,具体可以见 https://openjdk.org/jeps/200

Java 9 的 Module System 到底是什么?官方说法是:模块化在包之上增加了更高级别的聚合,它包括一组密切相关的包和资源以及一个新的模块描述符文件。简单点说,它是一个 Java 包的包 抽象。

目前 Module System 有 4 种类型的模块,如下表所示

类型 说明 备注
系统模块 Java SE 和 JDK 模块,通过 list-modules 可以看到完整列表 /
应用程序模块 业务自己定义的模块 /
自动模块 当将非模块 jar 添加到模块路径时,会创建具有 jar 名称的模块 1、默认导出所有包 2、默认情况下可以访问所有其他模块的类
未命名模块 当将 jar 或类添加到类路径时,所有这些类都会添加到未命名的模块中 1、只导出到其他未命名的模块和自动模块。这意味着,应用程序模块无法访问这些类 2、它可以访问所有模块的类

下面我们通过一个小案例来直观的体验下模块化,也就是上表中的 应用程序模块

模块案例

这个案例中包含两个模块,glmapper.modules 模块用于导出自己的服务,test.modules 模块用来测试引用第一个模块。

模块1 - glmapper.modules

  • 创建项目文件夹
1
2
mkdir my-project
cd my-project
  • 创建模块目录
1
mkdir my-module 
  • 在 my-module 目录下创建 glmapper.modules
    1
    mkdir glmapper.modules 
  • 在模块下创建 package
    1
    com.glmapper.bridge.boot
  • 包中创建一个名为 HelloModules.java的新类
    1
    2
    3
    4
    5
    6
    7
    package com.glmapper.bridge.boot;

    public class HelloModules {
    public static void sayHello() {
    System.out.println("Hello, Glmapper Modules!");
    }
    }
  • glmapper.modules根目录中添加模块描述符 module-info.java
    1
    2
    3
    4
    module glmapper.modules {
    // 导出 com.glmapper.bridge.boot 包的所有公共成员
    exports com.glmapper.bridge.boot;
    }

此时的文件目录大致如下:

1
2
3
4
5
6
7
8
└── my-module
└── glmapper.modules
├── com
│   └── glmapper
│   └── bridge
│   └── boot
│   └── HelloModules.java
└── module-info.java

模块2 - test.modules

  • 在 my-module 下创建 test.modules 模块
    1
    mkdir test.modules
  • 创建模块描述符文件 module-info.java
    1
    2
    3
    module test.modules {
    requires glmapper.modules;
    }
  • 创建 com.glmapper.bridge.main 包,并创建一个 TestMain.java 文件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.glmapper.bridge.main;
    // 导入 glmapper.modules 的类依赖
    import com.glmapper.bridge.boot.HelloModules;

    public class TestMain {
    public static void main(String[] args) {
    // 调用依赖类的静态方法
    HelloModules.sayHello();
    }
    }

此时的目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
└── my-module
├── glmapper.modules
│   ├── com
│   │   └── glmapper
│   │   └── bridge
│   │   └── boot
│   │   └── HelloModules.java
│   └── module-info.java
└── test.modules
├── com
│   └── glmapper
│   └── bridge
│   └── main
│   └── TestMain.java
└── module-info.java

构建运行模块

  • 构建模块()

    1
    2
    // modules 是构建产物的输出目录
    javac -d modules --module-source-path my-module $(find my-module -name "*.java")
  • 构建之后的目录结构如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    ├── modules // 构建产物所在的目录
    │   ├── glmapper.modules
    │   │   ├── com
    │   │   │   └── glmapper
    │   │   │   └── bridge
    │   │   │   └── boot
    │   │   │   └── HelloModules.class
    │   │   └── module-info.class
    │   └── test.modules
    │   ├── com
    │   │   └── glmapper
    │   │   └── bridge
    │   │   └── main
    │   │   └── TestMain.class
    │   └── module-info.class
    └── my-module
    ├── glmapper.modules
    │   ├── com
    │   │   └── glmapper
    │   │   └── bridge
    │   │   └── boot
    │   │   └── HelloModules.java
    │   └── module-info.java
    └── test.modules
    ├── com
    │   └── glmapper
    │   └── bridge
    │   └── main
    │   └── TestMain.java
    └── module-info.java
  • 运行代码

    1
    2
    3
    # 运行模块需要指定 模块路径和主类 
    > java --module-path modules -m test.modules/com.glmapper.bridge.main.TestMain
    > Hello, Glmapper Modules!

    可以看到,我们得到了正确的结果。

那么我们再来测试一种场景,就是在 glmapper.modules 中不导出包,重新构建时得到的结果如下:

1
2
3
4
5
6
my-module/test.modules/com/glmapper/bridge/main/TestMain.java:3: 错误: 程序包 
com.glmapper.bridge.boot 不可见
import com.glmapper.bridge.boot.HelloModules;
^
(程序包 com.glmapper.bridge.boot 已在模块 glmapper.modules 中声明, 但该模块未导出它)
1 个错误

可以看到,当编译 test.modules 时,会检测出它所依赖的模块中的 package 是否被导出,如果没有导出那么就无法通过编译。

接口使用

上面案例中,为了便于测试,是在模块中提供了一个可访问的静态方法;下面我们继续改造,在 glmapper.modules 中提供 interface 以及 interface 的实现,并通过使用 provides…with 和 uses 指令来实现和 test.modules 模块的交互引用。

  • 在 glmapper.modules 中提供一个 HelloService 接口
    1
    2
    3
    4
    package com.glmapper.bridge.boot;
    public interface HelloService {
    void helloWorld();
    }
  • HelloService 接口实现类 HelloServiceImpl
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package com.glmapper.bridge.boot.impl;

    import com.glmapper.bridge.boot.HelloService;

    public class HelloServiceImpl implements HelloService {

    public void helloWorld() {
    System.out.println("Hello World...");
    }
    }
  • 修改 glmapper.modules/module-info.java
    1
    2
    3
    4
    5
    module glmapper.modules {
    // 导出 com.glmapper.bridge.boot 包的所有公共成员
    exports com.glmapper.bridge.boot;
    provides com.glmapper.bridge.boot.HelloService with com.glmapper.bridge.boot.impl.HelloServiceImpl;
    }
  • 修改 test.modules/module-info.java
    1
    2
    3
    4
    module test.modules {
    requires glmapper.modules;
    uses com.glmapper.bridge.boot.HelloService;
    }
  • 修改 TestMain
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package com.glmapper.bridge.main;

    import com.glmapper.bridge.boot.HelloModules;
    import com.glmapper.bridge.boot.HelloService;
    import com.glmapper.bridge.boot.impl.HelloServiceImpl;
    import java.util.ServiceLoader;

    public class TestMain {
    public static void main(String[] args) {
    HelloModules.sayHello();
    // 这里可以通过 ServiceLoader SPI 方式来调用的
    Iterable<HelloService> services = ServiceLoader.load(HelloService.class);
    HelloService service = services.iterator().next();
    service.helloWorld();

    // 通过实例化对象调用
    HelloService helloService = new HelloServiceImpl();
    helloService.helloWorld();
    }
    }
  • 重新编译并执行
    1
    2
    3
    4
    > java --module-path modules -m test.modules/com.glmapper.bridge.main.TestMain    
    > Hello, Glmapper Modules!
    Hello World...
    Hello World...

小坑

module-info.java 中导出包不能支持对其子包的导出

1
2
3
4
5
6
7
8
module glmapper.modules {
// 导出 com.glmapper.bridge.boot 包的所有公共成员
exports com.glmapper.bridge.boot;
// 这里需要 com.glmapper.bridge.boot.impl,否则 impl 子包中的内容对 test.modules 不可用
exports com.glmapper.bridge.boot.impl;
provides com.glmapper.bridge.boot.HelloService with
com.glmapper.bridge.boot.impl.HelloServiceImpl;
}

总结

本文对 Java 模块化进行了介绍,通过本文你可以大体了解到 Java 模块化发展的基本情况,了解 Java 9 提供的模块化能力和 OSGI 模块化能力的差异。然后我通过一个案例向你介绍了 Java 模块化的基本使用方式,希望对你能够有所帮助。

参考

https://www.infoq.com/articles/java9-osgi-future-modularity/
https://www.oracle.com/corporate/features/understanding-java-9-modules.html
https://www.baeldung.com/java-9-modularity#3-module-descriptor

作者

卫恒

发布于

2022-12-04

更新于

2022-12-04

许可协议

评论