Content
如何使用模块--从编写源代码到编译,打包和运行程序。
使用如下目录层次结构来编写,编译,打包和运行源代码:
src目录用于保存源代码,其中包含一个com.jdk9.m的子目录,并且创建一个同名的com.jdk9.m模块名,并将其源代码保存在整个子目录下。注:这个子目录不一定要与模块名相同。
mods目录将已编译的代码保存在展开的目录层次结构中。如果需要,可以使用此目录中的代码运行应用程序。
lib存储打包成一个模块化的JAR,可以使用模块化JAR来运行程序,也可以将模块JAR提供给可以运行程序的其他开发人员。
创建一个名为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 class ;
Module mod = cls.getModule();
String modName = mod.getName();
System.out.println( "Module Name: " + modName);
}
}
|
最终的目录结构如下:
使用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文件中。
将模块的编译代码打包成一个模块化的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命令时将用作设置当前目录。
使用java命令来运行java程序:
语法:java --module-path
> java --module-path lib --module com.jdk.m/com.jdk.m.Welcome
这里,--module-path用于定位模块的模块路径。
--module指定与其主类一起运行的模块。
通过eclipse的工程创建向导创建一个Java工程,创建完成之后,需要注意,现在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 {}
|
运行Human.main,结果如下:
使用模块开发Java应用程序不会改变Java类型被组织成包的方式,模块的源代码在包层次结构的根目录下包含一个module-info.java文件,也就是说,module-info.java文件放在未命令的包中,它包含模块声明。
JDK9最重要和最令人激动的功能之一是模块系统,该模块系统是以代码名称Jigsaw的项目开发的。
在JDK9之前,开发一个Java应用程序通常包括以下步骤:
20多年来,Java社区以这种编写,编译,打包和部署Java代码的方式开发。这样部署和运行Java代码存在如下问题:
这些问题在Java社区中非常频繁,得到一个名字--JAR-Hell。
Java9通过引入开发,打包和部署Java应用程序的新方法来解决这些问题,在Java9中,Java应用程序由称为模块的小型交互组件组成,Java9已经将JDK/JRE组织为一组模块。
Java9引入了一个称为模块的新的程序组件,可以将Java程序视为具有明确定义的边界和这些模块之间依赖关系的交互模块的集合。模块系统的开发具有以下目标:
(1)可靠的配置
(2)强封装
(3)模块化JDK/JRE
可靠的配置解决了用于查找类型的容易出错的类路径机制的问题,模块必须声明对其他模块的显示依赖。模块系统验证应用程序开发的所有阶段的依赖关系--编译时,链接时和运行时。假设一个模块声明对另一模块的依赖,并且第二个模块在启动时丢失,JVM检测到依赖关系丢失,并在启动时失败,在Java9之前,当使用缺少的类型时,这样的应用程序会生成运行时错误(不是启动时)。
强大的封装解决了类路径上跨JAR的公共类型的可访问性问题。模块必须明确声明其中哪些公共类型可以被其他模块访问。
模块是代码和数据集合,它可以包含Java代码和本地代码。
对于Java代码,模块可以看做零个或多个包的集合。除了其名称,模块定义包括以下内容:
假设有两个模块com.jdk9.human:包含Human类,com.jdk9.address:包含Address类;
其中Human想使用Address类,其模块图如下:
在eclipse中,创建两个名为human和address的Java项目,每个项目都将包含与项目名称相同的模块的代码。
目录结构如下:
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;
}
|
运行结果如下:
此外,为了能够在human工程中直接引用address工程的com.jdk9.address模块,需要做如下设置:
右击address工程-->Build Path-->Configure Build Path...-->切换到Projects,然后设置模块路径关联,如下图:
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]
static:则
transitive:级联依赖;
JDK9之前,一个包中的public类型可以被前台包访问,没有任何限制。也就是说,包没有控制它们包含的类型的可访问性。JDK9中的模块系统对类型的可访问性提供了细粒度的控制。
模块之间的可访问性是所使用的模块和使用模块之间的双向协议:模块明确地使其公共类型可供其他模块使用,而且使用这些公共类型的模块明确声明对第一个模块的依赖,模块中所有未导出的软件包都是模块的私有的,他们不能再模块之外使用。
将包中的API设置为公共供其他模块使用被称之为导出包。
模块系统只知道一个模块:java.base,java.base模块不依赖与任何其他模块,所有其他模块都隐含地依赖于java.base模块。
构建模块图旨在编译时,链接时和运行时解析模块依赖关系,模块解析从根模块开始,并遵循依赖关系链接,直到达到java.base模块。
可以创建一个不包含任何代码的模块,它收集并重新导出其他模块的内容,这样的模块称为聚合模块。假设有一个模块依赖于五个模块,可以为这五个模块创建一个聚合模块,现在你的模块只要依赖于一个模块--聚合模块。
本节包含用于声明模块的语法的快速概述。使用模块声明来定义模块,是Java编程语言中的新概念:
[open] module
......
}
open修饰符是可选的,它声明一个开放的模块,一个开放的模块导出所有的包,以便其他模块使用反射访问。
模块名称可以是Java限定标识符,与包命名约定类型,使用反向域名模式为模块提供唯一的名称。
JDK9中,open,module,requires,transitive,exports,opens,to,uses,provices,with是受限关键字,只有在具体位置出现在模块声明中时,它们才具有特殊意义。可以将它们用作程序中其他地方的标识符。
(1)将包拆分成多个模块是不允许的,也就是说,同一个包不能在多个模块中定义;
(2)不能同时访问多个模块中的相同软件包;
(3)模块图不能包含循环依赖,也就是说两个模块不能彼此读取,如果需要,他们应该是一个模块,而不是两个;
(4)模块声明不支持模块版本,需要使用jar工具或其他一些工具(javac)将模块的版本添加为类文件属性;
(4)模块系统没有子模块的概念,com.jdk9.address和com.jdk9.address.child是两个单独的模块。
导出语句将模块的指定包导出到所有模块或编译时和运行时的命令模块列表。形式如下:
exports
假设需要开发多个模块组成的库或框架,其中有一个模块中的包含API,仅供某些模块内部使用。也就是说,该模块中的包不需要导出到所有模块,而是其可访问性必须限于几个命名的模块,可以使用模块声明中的限定的export to语句来实现:
exports
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;
}
Java允许使用反射机制访问所有成员,包括私有,公共,包和受保护的类型。需要在成员对象上调用setAccessible(true)方法。
模块系统提供如下规则:
开放语句允许对所有模块的反射访问指定的包或运行时指定的模块列表。其他模块可以反射访问指定包中的所有类型以及这些类型的所有成员(私有和公共),开放语句采用如下形式:
opens
opens
open com.jdk9.address {
exports xxx;
requires xxx;
uses xxx;
provides xxx;
// 不允许opens
}
定义com.jdk9.address模块是一个开放模块,其他模块可以在本模块中的所有软件包上对所有类型使用深层反射。可以在开放模块中声明exports,requires,uses和provides语句,但不能再opens的模块中再声明opens语句。opens语句用于打开特定的包以进行深层反射,因为开放模块打开所有的软件包进行深层反射,所以在开放模块中不允许再使用open语句。
打开一个包意味着其他模块对该包中的类型使用深层反射,可以打开一个包指定给所有其他模块或特定的模块列表,打开一个包到所有其他模块的打开语句的语法如下:
opens
opens
在JDK9之前,有4中访问类型:
在JDK8中,public类型意味着程序的所有部分都可以访问它,在JDK9中,public类型可能不是对每个类都公开的,模块中定义的public类型可能分为3类:
模块系统在编译时以及运行时验证模块的依赖关系,有事希望在编译时模块依赖性是必需的,但在运行时是可选的。
需要(require)语句声明当前模块对另一个模块的依赖关系,
requires
requires transitive
requires static
requires transitive static
static标示在编译时的依赖是强制的,但在运行时是可选的:requires static N意味着模块M需要模块N,模块N必须在编译时出现才能编译模块M,而在运行时存在模块N是可选的。
transitive当前模块依赖其他模块具有隐式依赖性,假设有三个模块P,Q和R,假设模块Q包含requires transitive R语句,如果模块P包含requires Q,这意味着模块P隐含依赖模块R。
Java允许使用服务提供者和服务使用者分离的服务提供者机制。JDK9运行使用语句uses和provides实现其服务。
use语句可以指定服务接口的名字,当前模块就会发现它,使用java.util.ServiceLoader类进行加载:
uses
实例:
module M {
uses com.jdk9.prime.PrimeChecker;
}
com.jdk9.prime.PrimeChecker是一个服务接口,其实现类将由其他模块提供,模块M将使用java.util.ServiceLoader类来发现和加载此接口的实现。
provide语句指定服务接口的一个或多个服务厅程序实现类:
provide
实例:
module P {
uses com.jdk9.CsvParser;
provides com.jdk9.CsvParser with com.jdk9.CsvParserImpl;
provides com.jdk9.prime.PrimeChecker with com.jdk9.prime.PrimeCheckerImpl;
}
模块声明存储在名为module-info.java的文件中,该文件存储在该模块的源文件层次结构的根目录下。
Java编译器将模块声明编译为名为module-info.class的文件。module-info.class文件被称为模块描述符。它被放置在模块的编译代码层次结构的根目录下。如果将模块的编译代码打包到jar文件中,则module-info.class文件将存储在jar文件的根目录下。
在模块系统的初始原型中,模块声明还包括模块版本。包括模块版本在声明中使模块系统的实现复杂化,所以模块版本从声明中删除。模块描述符(类文件格式)的可扩展格式被利用来向模块添加版本。当将模块的编译代码打包到jar中时,该jar工具提供了一个添加模块版本的选项,最后将其添加到module-info.class文件中。
模块的artifact可以存储在:
当模块的编译代码存储在目录中时,目录的根目录包含模块描述符(module-info.class文件),子目录是包层次结构的镜像。
当JAR包含模块的编译代码时,JAR称为模块化JAR。模块化JAR在根目录下包含一个module-info.class文件。
无论在JDk9之前使用JAR,现在都可以使用模块化JAR。例如,模块化JAR可以放置在类路径上,在这种情况下,模块化JAR中的module-info.class文件将被忽略,因为module-info中不是有效的类名。
JDK9引入了一种称为JMOD的新格式来封装模块。JMOD文件使用.jmod扩展名。JDK模块被编译成JMOD格式,放在JDK_HOMEjmods目录中。例如,可以找到一个包含java.base模块内容的java.base.jmod文件。仅在编译时和链接时才支持JMOD文件。它们在运行时不受支持。
旧的和新的应用程序将继续使用未被模块化或永远不会被模块化的库,如果JDK9保持向后兼容性。在大多数情况下,在JDK8或更早版本中工作的应用程序将继续在JDK 9中工作,为了简化迁移,JDK9定义了4中类型的模块:
(1)一个模块是代码和数据的集合;
(2)基于模块是否具有名称,模块可以是命名模块或未命名模块;
(3)没有其他类别的未命名模块;
(4)当模块具有名称时,可以在模块声明中明确指定名称,或则可以自动(或隐式)生成名称,如果名称在模块声明中明确指定,则称为显式模块,如果名称由模块系统通过读取模块路径上的JAR文件名生成,则称为自动模块。
(5)如果不实用open修饰符的情况下声明模块,则称为普通模块;
(6)如果使用open修饰符声明模块,则称为开放模块。
开放模块也是显式模块和命名模块,自动模块是一个命名模块,因为它具有自动生成的名称,但它不是显式模块,因为它在模块系统在编译时和运行时被隐式声明。
使用模块声明明确而不实用open修饰符的模块始终被赋予一个名称,它被称为普通模块或简化模块。
模块声明包含open修饰符,则该模块被称为开放模块。
为了向后兼容,查找类型的类路径机制仍然可以在JDK9中使用,可以选择将JAR放在类路径、模块路径和两者组合上。请注意,可以在模块路径和类路径上放置模块化JAR以及JAR。
将JAR放在模块路径上时,JAR被视为一个模块,称为自动模块。
自动模块其实也是一个有名字的模块,其名称和版本由JAR文件的名称派生,规则如下:
下面列出了几个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
要有效使用的自动模块,必须导出包并读取其他模块:
可以将JAR和模块化JAR放在类路径上,当类型加载并且在任何已知模块中到不到其包时,模块系统会尝试从类路径加载类型。如果在类型路径上找到该类型,它将由类加载器加载,并成为该类加载器的一个名为unnamed模块的模块成员。每个类加载器定义一个未命名的模块,其成员是从类路径加载的所有类型。一个未命名的模块没有名次,因此显式模块不能使用requires语句来声明对它的依赖,如果有明确的模块需要时会用未命名模块中的类型,则必须通过将JAR放置在模块路径上,将未命名模块的JAR用作自动模块。
在JDK9之前,一个有意义的Java应用程序由几个驻留在三个层面的JAR组成:
JDK9已经通过将Java运行时JAR转换为模块来模块化,也就是说,Java运行时由模块组成。
类库层主要由放置在类路径上的第三方JAR组成,如果要将应用程序迁移到JDk9,可能无法获得第三方JAR的的模块化版本,也无法控制供应商如果将第三方JAR转换为模块。所以,可以将库JAR放在模块路径上,并将其视为自动模块。