JDK9-模块化系统

Content

Top

0. 实例

如何使用模块--从编写源代码到编译,打包和运行程序。

0.1 使用命令行编写和运行模块程序

0.1.1 设置目录

使用如下目录层次结构来编写,编译,打包和运行源代码:

JDK9-模块化系统

src目录用于保存源代码,其中包含一个com.jdk9.m的子目录,并且创建一个同名的com.jdk9.m模块名,并将其源代码保存在整个子目录***:这个子目录不一定要与模块名相同。

mods目录将已编译的代码保存在展开的目录层次结构中。如果需要,可以使用此目录中的代码运行应用程序。

lib存储打包成一个模块化的JAR,可以使用模块化JAR来运行程序,也可以将模块JAR提供给可以运行程序的其他开发人员。

0.1.2 编写源代码

创建一个名为module-info.java的文件,在文件中声明模块的代码:

1
2
3
module com.jdk9.m {
     
}  

JDK9中的每个Java类型都是模块的成员,甚至是int,long和char等原始类型。所有原始类型都是java.base模块的成员。JDK9中的Class类有一个名为getModule()的新方法,它返回该类作为其成员的模块引用。

注:所有原始数据类型都是java.base模块的成员,可以使用int.class.getModule()获取int基本类型的模块的引用。

Welcome类的源代码如下:

1
2
3
4
5
6
7
8
9
10
package com.jdk9.m
 
public class Welcome {
    public static void main(String[] args) {
        Class<Welcome> cls = Welcome.class;
        Module mod = cls.getModule();
        String modName = mod.getName();
        System.out.println("Module Name: " + modName);
    }
}

最终的目录结构如下:

JDK9-模块化系统

0.1.3 编译

使用javac命令编译远点并将编译的代码保存在mods目录下。

 > javac -d mods --module-source-path src src\com.jdk.m\module-info.java src\com.jdk.m\Welcome.java

-d mods将所有编译的类文件保存到mods目录下。  

--modules-source-path src指定src目录的子目录包含多个模块的源代码,其中每个子目录名称与包含源代码的子目录的模块名称相同。

可以使用javac的--module-version选项,可以指定正在编译的模块的版本,模块版本保存在module-info.class文件中。 

JDK9-模块化系统

0.1.4 打包模块代码

将模块的编译代码打包成一个模块化的JAR。

> jar --create --file lib\com.jdk.m-1.0.jar --main-class com.jdk.m.Welcome --module-version 1.0 -C mods/com.jdk.m .

--create选项表示要创建一个新的模块化JAR。

--file选项用于指定新的模块化JAR的位置和名称,将新的模块化JAR保存在lib目录中。

--main-class指定main方法作为应用程序入口。

--module-version指定模块版本。

-C指定执行jar命令时将用作设置当前目录。

0.1.5 运行程序

使用java命令来运行java程序:

语法:java --module-path <module-path> --module <module>/<main-class>

> java --module-path lib --module com.jdk.m/com.jdk.m.Welcome

这里,--module-path用于定位模块的模块路径。

--module指定与其主类一起运行的模块。 

0.2 使用eclipse编写和运行模块程序

 0.2.1 创建Java工程

通过eclipse的工程创建向导创建一个Java工程,创建完成之后,需要注意,现在JDK9的类已经以模块化的形式进行管理了,如图:

JDK9-模块化系统

0.2.2 目录结构

 JDK9-模块化系统

human的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.jdk9.human;
 
public class Human {
    private String name;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
    public static void main(String[] args) {
        Module m = Human.class.getModule();
        System.out.println(m);
    }
 
}

  

module-info.java的源码如下:

1
module com.jdk9.human {}  

 0.2.3 运行

运行Human.main,结果如下:

JDK9-模块化系统

使用模块开发Java应用程序不会改变Java类型被组织成包的方式,模块的源代码在包层次结构的根目录下包含一个module-info.java文件,也就是说,module-info.java文件放在未命令的包中,它包含模块声明。

Top

1. 背景

JDK9最重要和最令人激动的功能之一是模块系统,该模块系统是以代码名称Jigsaw的项目开发的。

在JDK9之前,开发一个Java应用程序通常包括以下步骤:

  • Java源代码以Java类型(类,接口,枚举和注释)的形式编写。
  • 不同的Java类型被安排在一个包中,而且始终属于一个明确或默认的包。
  • 编译的代码被打包成一个或多个jar文件,也称为应用程序jar。因为它们包含应用程序代码,一个程序包张的代码可能会引用多个jar。
  • 应用程序可能使用类库,类库作为一个或多个jar文件提供给应用程序使用。
  • 通过将所有jar文件,应用程序jar文件来jar类库放在类路径上来部署应用程序。

20多年来,Java社区以这种编写,编译,打包和部署Java代码的方式开发。这样部署和运行Java代码存在如下问题:

  • 一个包只是一个类型等待容器,而不强制执行任何可访问性边界:包中的公共类型可以在所有其他包中访问;没有办法阻止在一个包中公开类型的全局可见性。
  • 除了以java和javax开头的包外,包颖是开放扩展的。如果你在具有包级别访问的jar中进行类型化,则可以在其他jar中访问定义与你的名称相同的包中的类型。
  • Java运行时会看到从jar列表加载的一组包。没有办法知道是否在不同的jar中有多个相同类型的副本。Java运行时首先加载的类路径中遇到的jar中找到的类型。
  • Java运行时可能会出现由于应用程序在类路径中需要的其中一个jar引起的运行时缺少类型的情况,当代码尝试使用它们时,缺少的类型会引起运行时错误。
  • 在启动时没有办法知道应用程序中使用的某些类型已经丢失。还可以包含错误的jar文件版本,并在运行时产生错误。

这些问题在Java社区中非常频繁,得到一个名字--JAR-Hell。

Java9通过引入开发,打包和部署Java应用程序的新方法来解决这些问题,在Java9中,Java应用程序由称为模块的小型交互组件组成,Java9已经将JDK/JRE组织为一组模块。

Top

2. 全新的模块系统

Java9引入了一个称为模块的新的程序组件,可以将Java程序视为具有明确定义的边界和这些模块之间依赖关系的交互模块的集合。模块系统的开发具有以下目标:

(1)可靠的配置

(2)强封装

(3)模块化JDK/JRE

可靠的配置解决了用于查找类型的容易出错的类路径机制的问题,模块必须声明对其他模块的显示依赖。模块系统验证应用程序开发的所有阶段的依赖关系--编译时,链接时和运行时。假设一个模块声明对另一模块的依赖,并且第二个模块在启动时丢失,JVM检测到依赖关系丢失,并在启动时失败,在Java9之前,当使用缺少的类型时,这样的应用程序会生成运行时错误(不是启动时)。

强大的封装解决了类路径上跨JAR的公共类型的可访问性问题。模块必须明确声明其中哪些公共类型可以被其他模块访问。

Top

3. 什么是模块化

模块是代码和数据集合,它可以包含Java代码和本地代码。

对于Java代码,模块可以看做零个或多个包的集合。除了其名称,模块定义包括以下内容:

  • requires其他模块(或依赖于)的列表
  • exports包列表(其public API),其他模块可以使用
  • open的包(整个API,共有和私有),其他模块可以反射调用
  • 使用的服务列表
  • 提供的服务的实现列表
Top

4. 模块依赖关系

假设有两个模块com.jdk9.human:包含Human类,com.jdk9.address:包含Address类;

其中Human想使用Address类,其模块图如下:

JDK9-模块化系统

在eclipse中,创建两个名为human和address的Java项目,每个项目都将包含与项目名称相同的模块的代码。

目录结构如下:

JDK9-模块化系统

 address工程的主要代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.jdk9.address;
 
public class Address {
    private String line = "1111 Main Blvd";
    private String city = "Jacksonville";
    private String state = "FL";
    private String zip = "32256";
 
    public Address() {}
 
    public Address(String line, String city, String state, String zip) {
        this.line = line;
        this.city = city;
        this.state = state;
        this.zip = zip;
    }
 
    @Override
    public String toString() {
        return "[line: " + line + ", city: " + city + ", state: " + state + ", zip: " + zip + "]";
    }
 
}
1
2
3
module com.jdk9.address {
    exports com.jdk9.address;
}  

human工程的主要代码如下:

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
package com.jdk9.human;
 
import com.jdk9.address.Address;
 
public class Human {
    private String name;
    private Address address = new Address();
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
     
    public Address getAddress() {
        return address;
    }
 
    public void setAddress(Address address) {
        this.address = address;
    }
 
    public static void main(String[] args) {
        Human human = new Human();
        System.out.println(human.getAddress());
    }
}
1
2
3
module com.jdk9.human {
    requires com.jdk9.address; 
}  

运行结果如下:

JDK9-模块化系统

 此外,为了能够在human工程中直接引用address工程的com.jdk9.address模块,需要做如下设置:

右击address工程-->Build Path-->Configure Build Path...-->切换到Projects,然后设置模块路径关联,如下图:

 JDK9-模块化系统

exports语句用于将包导出到所有其他模块或某些命名模块,导出的包中的所有公共类型都可以在编译时和运行时访问。在运行时,可以使用反射来访问公共类型的公共成员。即使在这些成员上使用setAccessible(true)方法,公共类型的非公共成员也无法使用反射。 该语句将包中的所有公共类型导出到所有其他模块。

com.jdk9.address模块导出com.jdk9.address包,因此Address类可以由其他模块使用,它是公共的,也可以在com.jdk9.human包中使用。

Human类在com.jdk9.human模块中,它使用com.jdk9.address模块中的Address类型中的字段。这意味着com.jdk9.human模块读取com.jdk9.address模块。这通过com.jdk9.human模块中声明requires语句。

requires语句用于指定一个模块对另一个模块的依赖,requires语法如下:

requires [transitive] [static] <module>;

<module>:是当前模块读取的另一个模块的名称;

static:则<module>模块在编译时是必需的,但在运行时是可选的;

transitive:级联依赖;

 

JDK9之前,一个包中的public类型可以被前台包访问,没有任何限制。也就是说,包没有控制它们包含的类型的可访问性。JDK9中的模块系统对类型的可访问性提供了细粒度的控制。

模块之间的可访问性是所使用的模块和使用模块之间的双向协议:模块明确地使其公共类型可供其他模块使用,而且使用这些公共类型的模块明确声明对第一个模块的依赖,模块中所有未导出的软件包都是模块的私有的,他们不能再模块之外使用。

将包中的API设置为公共供其他模块使用被称之为导出包。

模块系统只知道一个模块:java.base,java.base模块不依赖与任何其他模块,所有其他模块都隐含地依赖于java.base模块。

构建模块图旨在编译时,链接时和运行时解析模块依赖关系,模块解析从根模块开始,并遵循依赖关系链接,直到达到java.base模块。

Top

5. 聚合模块

可以创建一个不包含任何代码的模块,它收集并重新导出其他模块的内容,这样的模块称为聚合模块。假设有一个模块依赖于五个模块,可以为这五个模块创建一个聚合模块,现在你的模块只要依赖于一个模块--聚合模块。

Top

6. 声明模块

本节包含用于声明模块的语法的快速概述。使用模块声明来定义模块,是Java编程语言中的新概念:

[open] module <moduleName> {

  <module-statement>;

  ......

}

open修饰符是可选的,它声明一个开放的模块,一个开放的模块导出所有的包,以便其他模块使用反射访问。<moduleName>是要定义的模块的名称,<module-statement>是一个模块语句。模块声明中可以包含零个或多个模块语句:

  • 导出语句(exports),导出模块,其他模块访问。
  • 开放语句(opens),开放当前模块,其他模块可以访问,包括反射调用等。
  • 需要语句(requires),声明模块对另一个模块的依赖关系。
  • 使用语句(uses),表达服务消费。
  • 提供语句(provides),表达服务提供。

6.1 模块命名

6.1.1 模块命名关键字

模块名称可以是Java限定标识符,与包命名约定类型,使用反向域名模式为模块提供唯一的名称。

JDK9中,open,module,requires,transitive,exports,opens,to,uses,provices,with是受限关键字,只有在具体位置出现在模块声明中时,它们才具有特殊意义。可以将它们用作程序中其他地方的标识符。

6.1.2 模块命名约束

(1)将包拆分成多个模块是不允许的,也就是说,同一个包不能在多个模块中定义;

(2)不能同时访问多个模块中的相同软件包;

(3)模块图不能包含循环依赖,也就是说两个模块不能彼此读取,如果需要,他们应该是一个模块,而不是两个;

(4)模块声明不支持模块版本,需要使用jar工具或其他一些工具(javac)将模块的版本添加为类文件属性;

(4)模块系统没有子模块的概念,com.jdk9.address和com.jdk9.address.child是两个单独的模块。

6.2 模块的访问控制

6.2.1 exports

导出语句将模块的指定包导出到所有模块或编译时和运行时的命令模块列表。形式如下:

exports <package>;

假设需要开发多个模块组成的库或框架,其中有一个模块中的包含API,仅供某些模块内部使用。也就是说,该模块中的包不需要导出到所有模块,而是其可访问性必须限于几个命名的模块,可以使用模块声明中的限定的export to语句来实现:

exports <package> to <module1>, <module2>;

package:当前模块要导出的包的名称;

module1,module2……:可以读取当前模块的模块的名称。

实例,包含非限定导出和限定导出:

module com.jdk9.module {

  exports com.jdk9.module.core;

  exports com.jdk9.module.util to com.jdk9.module.internal, com.jdk9.module.server;

}

6.2.2 opens

Java允许使用反射机制访问所有成员,包括私有,公共,包和受保护的类型。需要在成员对象上调用setAccessible(true)方法。

模块系统提供如下规则:

  • exports的包将允许在编译和运行时访问public类型及其public成员,如果不exports包,则该包中的所有类型都不可访问其他模块;
  • 可以打开一个模块,以便在运行时对该模块中的所有包中的所有类型进行深层反射,这样的模块称为开放模块;

开放语句允许对所有模块的反射访问指定的包或运行时指定的模块列表。其他模块可以反射访问指定包中的所有类型以及这些类型的所有成员(私有和公共),开放语句采用如下形式:

opens <package>;

opens <package> to <module1>, <module2>;

6.2.3 开放模块

open com.jdk9.address {

  exports xxx;

  requires xxx;

  uses xxx;

  provides xxx;

  // 不允许opens

}

定义com.jdk9.address模块是一个开放模块,其他模块可以在本模块中的所有软件包上对所有类型使用深层反射。可以在开放模块中声明exports,requires,uses和provides语句,但不能再opens的模块中再声明opens语句。opens语句用于打开特定的包以进行深层反射,因为开放模块打开所有的软件包进行深层反射,所以在开放模块中不允许再使用open语句。

6.2.4 打开包

打开一个包意味着其他模块对该包中的类型使用深层反射,可以打开一个包指定给所有其他模块或特定的模块列表,打开一个包到所有其他模块的打开语句的语法如下:

opens <package>;

opens <package> to <module1>, <module2>……

<package>仅用于深层反射到<module1>,<module2>等。 

6.2.5 访问类型

在JDK9之前,有4中访问类型:

  • public
  • protected
  • <package>
  • private

在JDK8中,public类型意味着程序的所有部分都可以访问它,在JDK9中,public类型可能不是对每个类都公开的,模块中定义的public类型可能分为3类:

  • 仅在定义模块内公开:如果一个类型在模块中被定义为public,但是该模块不导出包含该类型的包,则该类型仅在该模块中是公开的,没有其他模块而已访问类型;
  • 只针对特定模块公开:如果一个类型在一个模块中被定义为public,但是该模块使用一个限定的export来导出包含该类型的包,该类型将只能在有限导出的子句中指定的模块中访问;
  • 指定所有类公开:如果一个类型在模块中被定义为public,但该模块使用包含该类型的非限定的导出语句导出该包,该类型将公开给读取第一个模块的每个模块。

6.3 声明依赖关系

模块系统在编译时以及运行时验证模块的依赖关系,有事希望在编译时模块依赖性是必需的,但在运行时是可选的。 

需要(require)语句声明当前模块对另一个模块的依赖关系,

requires <module>;

requires transitive <module>;

requires static <module>;

requires transitive static <module>;

static标示在编译时的依赖是强制的,但在运行时是可选的:requires static N意味着模块M需要模块N,模块N必须在编译时出现才能编译模块M,而在运行时存在模块N是可选的。

transitive当前模块依赖其他模块具有隐式依赖性,假设有三个模块P,Q和R,假设模块Q包含requires transitive R语句,如果模块P包含requires Q,这意味着模块P隐含依赖模块R。

6.4 配置服务

Java允许使用服务提供者和服务使用者分离的服务提供者机制。JDK9运行使用语句uses和provides实现其服务。

use语句可以指定服务接口的名字,当前模块就会发现它,使用java.util.ServiceLoader类进行加载:

uses <service-interface>

实例:

module M {

  uses com.jdk9.prime.PrimeChecker;

}

com.jdk9.prime.PrimeChecker是一个服务接口,其实现类将由其他模块提供,模块M将使用java.util.ServiceLoader类来发现和加载此接口的实现。

provide语句指定服务接口的一个或多个服务厅程序实现类:

provide <service-interface> with <service-impl-class1>, <service-impl-class2>;

实例:

module P {

  uses com.jdk9.CsvParser;

  provides com.jdk9.CsvParser with com.jdk9.CsvParserImpl;

  provides com.jdk9.prime.PrimeChecker with com.jdk9.prime.PrimeCheckerImpl;

}

Top

7. 模块描述符

 7.1 编译模块声明

模块声明存储在名为module-info.java的文件中,该文件存储在该模块的源文件层次结构的根目录下。

Java编译器将模块声明编译为名为module-info.class的文件。module-info.class文件被称为模块描述符。它被放置在模块的编译代码层次结构的根目录下。如果将模块的编译代码打包到jar文件中,则module-info.class文件将存储在jar文件的根目录下。

7.2 模块版本

在模块系统的初始原型中,模块声明还包括模块版本。包括模块版本在声明中使模块系统的实现复杂化,所以模块版本从声明中删除。模块描述符(类文件格式)的可扩展格式被利用来向模块添加版本。当将模块的编译代码打包到jar中时,该jar工具提供了一个添加模块版本的选项,最后将其添加到module-info.class文件中。

Top

 8. 打包模块

模块的artifact可以存储在:

  • 目录中
  • 模块化的JAR文件中
  • JMOD文件中,它是JDK9中引入的一种新的模块封装格式

8.1 目录中的模块

当模块的编译代码存储在目录中时,目录的根目录包含模块描述符(module-info.class文件),子目录是包层次结构的镜像。

8.2 模块化JAR中的模块

当JAR包含模块的编译代码时,JAR称为模块化JAR。模块化JAR在根目录下包含一个module-info.class文件。

无论在JDk9之前使用JAR,现在都可以使用模块化JAR。例如,模块化JAR可以放置在类路径上,在这种情况下,模块化JAR中的module-info.class文件将被忽略,因为module-info中不是有效的类名。

8.3 JMOD文件中的模块

JDK9引入了一种称为JMOD的新格式来封装模块。JMOD文件使用.jmod扩展名。JDK模块被编译成JMOD格式,放在JDK_HOMEjmods目录中。例如,可以找到一个包含java.base模块内容的java.base.jmod文件。仅在编译时和链接时才支持JMOD文件。它们在运行时不受支持。

Top

9 模块的类型 

9.1 概述

旧的和新的应用程序将继续使用未被模块化或永远不会被模块化的库,如果JDK9保持向后兼容性。在大多数情况下,在JDK8或更早版本中工作的应用程序将继续在JDK 9中工作,为了简化迁移,JDK9定义了4中类型的模块:

  • 普通模块(Normal Modules)
  • 开放模块(Open Modules)
  • 自动模块(Automatic Modules)
  • 未命名模块(Unnameed Modules)

JDK9-模块化系统

(1)一个模块是代码和数据的集合;

(2)基于模块是否具有名称,模块可以是命名模块或未命名模块;

(3)没有其他类别的未命名模块;

(4)当模块具有名称时,可以在模块声明中明确指定名称,或则可以自动(或隐式)生成名称,如果名称在模块声明中明确指定,则称为显式模块,如果名称由模块系统通过读取模块路径上的JAR文件名生成,则称为自动模块。

(5)如果不实用open修饰符的情况下声明模块,则称为普通模块;

(6)如果使用open修饰符声明模块,则称为开放模块。

开放模块也是显式模块和命名模块,自动模块是一个命名模块,因为它具有自动生成的名称,但它不是显式模块,因为它在模块系统在编译时和运行时被隐式声明。

9.2 普通模块

使用模块声明明确而不实用open修饰符的模块始终被赋予一个名称,它被称为普通模块或简化模块。

9.3 开放模块

模块声明包含open修饰符,则该模块被称为开放模块。

9.4 自动模块 

为了向后兼容,查找类型的类路径机制仍然可以在JDK9中使用,可以选择将JAR放在类路径、模块路径和两者组合上。请注意,可以在模块路径和类路径上放置模块化JAR以及JAR。

将JAR放在模块路径上时,JAR被视为一个模块,称为自动模块。

自动模块其实也是一个有名字的模块,其名称和版本由JAR文件的名称派生,规则如下:

  • 删除jAR文件的.jar扩展名,如果JAR文件名是com.jdk9.m-1.0.jar,则此步骤将删除.jar扩展名,并通过以下步骤使用com.jdk9.m-1.0来推倒出模块的名称及其版本;
  • 如果名称以连字符后跟至少一个数字(也可后跟一个点),则模块名称将从最后一个连字符之前的名称部分派生,如果它可以被解析为有效的版本,连字符后面的部分被分配为模块的版本,在此示例中,模块名称将从com.jdk9.m派生,版本派生为1.0;
  • 名称部分中的每个非字母数字字符都被替换为一个点,并且在所得到的字符串中,用一个点替换两个连续的点,此外,所有前导和后跟的点都被删除。

下面列出了几个JAR名称,以及派生的自动模块名称和版本:

JAR名词 模块名称
com.jdk9.m-1.0 com.jdk9.m
junit-4.10 junit
apache-logging1.5.0 有错误
spring-core-4.0.1.RELEASE spring.core
jdojo-tans-api_1.5_spec-1.0.0 有错误
- 有错误

如果无法从其名称导出有效的自动模块名称,则放置在模块路径上的JAR将抛出异常:
java.lang.module.ResolutionException: Unable to derive module desciptor for: apache-logging1.5.0.jar

要有效使用的自动模块,必须导出包并读取其他模块:

  • 自动模块读取所有其他 模块,在解析模块图之后,会添加自动OK到所有其他模块,其他模块都可以读取自动模块;
  • 自动模块中的所有包都被导出并打开

9.5 未命名模块

可以将JAR和模块化JAR放在类路径上,当类型加载并且在任何已知模块中到不到其包时,模块系统会尝试从类路径加载类型。如果在类型路径上找到该类型,它将由类加载器加载,并成为该类加载器的一个名为unnamed模块的模块成员。每个类加载器定义一个未命名的模块,其成员是从类路径加载的所有类型。一个未命名的模块没有名次,因此显式模块不能使用requires语句来声明对它的依赖,如果有明确的模块需要时会用未命名模块中的类型,则必须通过将JAR放置在模块路径上,将未命名模块的JAR用作自动模块。

Top

10 迁移到JDK9

在JDK9之前,一个有意义的Java应用程序由几个驻留在三个层面的JAR组成:

  • 开发人员开发的应用程序层中的程序JAR;
  • 在类库层中的类库JAR--通常由第三方提供;
  • JVM层中的Java运行时的JAR;

JDK9已经通过将Java运行时JAR转换为模块来模块化,也就是说,Java运行时由模块组成。

类库层主要由放置在类路径上的第三方JAR组成,如果要将应用程序迁移到JDk9,可能无法获得第三方JAR的的模块化版本,也无法控制供应商如果将第三方JAR转换为模块。所以,可以将库JAR放在模块路径上,并将其视为自动模块。