模块的概念
在Java系统中,提供了多种封装的层级。
在面向对象编程中,基础的构建要素就是类。类提供了封装,私有特征只能被具有明确访问权限的代码访问。
包提供了更高一级的组织方式,包是类的集合。包也提供了一种封装级别,具有包访问权限的所有特征(无论是公有还是私有)都只能被同一个包中的方法访问。
但是在大型系统中,这些控制级别还是显得不够,所以设计了模块的概念。
一般来说,一个Java平台模块包含以下部分:
- 一个包集合。
- 可选地包含资源文件和像本地库这样的其他文件。
- 一个有关模块中可访问的包的列表。
- 一个有关这个模块依赖的所有其他模块的列表。
对模块命名
模块是包的集合。模块中的包名无需彼此相关。并且模块名和包名是可以完全相同的。
模块名是由字母、数字、下划线和句点构成的。而且,和路径名一样,模块之间没有任何层次关系。如priv.mw
与priv.mw.dao
是完全不同的两个模块。
一般来说模块的名字与包名类似,都遵循“反向域名”惯例,就像和包名一样。命名模块最简单的方式就是按照模块提供的顶级包来命名。这个惯例可以防止模块中产生报名冲突,因为任何给定的模块都只能被放到一个模块中。
一个例子:模块化的”Hello World!”程序
我们要将一个普通程序改为模块化,可以按照以下步骤:
类必须放置在一个具名的包中(不具名包是不能包含在模块中的),代码如下:
1
2
3
4
5
6
7package priv.mw.hello;
public class HelloWorld{
public static void main(String[] args){
System.out.println("Hello world!");
}
}创建一个
module-info.java
模块声明文件,用于存储模块的信息。该文件位于基目录中(即,与包含com
目录的目录相同)。按照惯例,基目录的名字与模块名相同,目录结构如下:1
2
3
4
5
6mymudule.hellomod/
module-info.java
priv/
mw/
hello/
HelloWorld.javamodule-info.java
文件包含模块声明:1
2
3module mymudule.hellomod
{
}这个模块声明之所以为空,是因为该模块没有任何可以向其他人提供的内容,也不需要依赖任何其他模块。
module-info.java
这个文件与一般的Java资源文件不同。从名字上也能看出来。因为类名不能包含连字符。在该文件中,用require
和exports
等”限定关键词(仅在模块声明中具有特殊意义)“来确定导入和导出的关键词。
对模块的需求
在Java9之后,jdk就被模块化了,其被分成了多个模块。每个模块都有一个module-info.java
用于确定导入导出的模块。由于java.base
包含的是Java最基础的类,所以这个模块是默认加载的,而其他的一些类别,如前面提到的ScriptEngineManager
都需要自己在module-info.java
文件中自己导入。
而模块中的还可能会依赖其他的模块,就会形成一个依赖链。例如下面的依赖链:
注意在模块依赖中不能有环,即,一个模块不能直接或间接地对自己产生依赖。
导出/导入包
导出包
在模块系统中,可以在module-info.java
使用关键字export
来指定导出的模块。例如,下面是java.xml
模块地模块声明中地一部分:
1 | module java.xml |
值得注意的是:
- 只有导出了的类才能被外部使用,也就是说可以隐藏内部地部分类。
导入包
同样地,可以使用requires
关键字来导入需要使用的模块。例如:
1 | module priv.mw |
注意exports
到处的是包,而requires
引用的是模块。
模块化的JAR
模块可以通过将其所有的类都置于一个JAR文件中而得已部署,其中module-info.class
在JAR文件的根部。这样的JAR文件被称为模块的JAR。
要想创建JAR文件,只需要以通常的方式使用jar
工具。如果有多个包,那么最好使用-d
选项来编译,这样可以将类文件置于单独的目录中,如果目录不存在,那么会创建该目录。然后,在收集这些类文件时使用-C
选项的jar
命令来修改该目录。
例如:
1 | javac -d modules/com.horstman.greet $(find com.horstman.greet -name *.java) |
如果使用Maven, Gradle
这样的构建工具,那么只需要按照管用的方式来构建jar。只要module-info.class
包含在内,就可以得到该模块的JAR文件。
然后,在模块路径中包含该模块化的JAR,该模块就会被加载。
也可以指定模块化的JAR中的主类:
1 | javac -p com.horstman.greet.jar -d modules/v2ch09.exportedpkg $(find v2ch09.exportedpkg -name *.java) |
当启动该程序时,可以指定包含主类的模块:
1 | java -o com.horstman.greet.jar:v2ch09.exportedpkg.jar -m v2ch09.exportedpkg |
模块和反射式访问
对于类,可以通过反射来克服其权限问题,但是在模块中,则不能再这样吧访问了。即:如果一个类位于某个模块中,那么非公有成员的反射式访问将失败。
理论上来讲,这种破坏封装的特性是不合理的,但是由于长期的存在和使用,Java为其设计了open
关键字。只要将某个模块的导出设定为open
,则其内部的变量都是可以通过反射来访问的,无论是公开还是私有。也可以单独确定模块中的某个包为开放的。例如:
- 模块开放
1 | open module xxx{ |
- 模块中的包开放
1 | module xxx{ |
值得注意的是:模块采用的是open
,而包采用的opens
关键字。
自动模块和不具名模块
为了过度(即Java9之前的应用都是没有模块系统的),Java设计了两个机制来缓解模块化前后的不兼容:
- 自动化模块
- 不具名模块
自动模块
如果是为了迁移,我们可以通过把任何JAR文件置于模块路径的目录而不是类路径的目录中,实现将其转换为一个模块。模块路径上没有module-info.class
文件的JAR被称为自动模块。自动模块具有下面的属性:
- 模块隐式的包含对其他所有模块的
requires
子句。 - 其所有包都被导出,且是开放的(
open
)。 - 如果在JAR清单
META-INF/MANIFEST.MF
中具有键为Automatic-Module-Name
的项,那么它的值会变为模块名。 - 否则,模块名将从
JAR
文件名中获得,具体为:将文件名中尾部的版本号删除,并将非字母数字的字符替换为句点。
前两条规则表明自动模块中的包的行为和在类路径上一样。使用模块路径的原因是为u了让其他模块受益,使得它们可以表示对这个模块的依赖关系。
不具名模块
任何不在模块路径中的类都是不具名模块的一部分。从技术上讲,可能会有多个不具名模块,但是它们合起来看就像是单个不具名的模块。与自动模块一样,不具名模块可以访问所有其他的模块,它的所有包都会被导出,并且都是开放的。
但是,没有任何明确模块可以访问不具名的模块。(明确模块是既不是自动模块也不是不具名模块,即,module-info.java
在模块路径上的模块。)
传递需求和静态需求
传递需求
前面提到,模块的需求是不会传递的,即A需要B,B需要C,则A并不会直接需要C。但是有时候这种需求又是存在的,比如一些列的包都是需要底层的一个包,可以使用requires transitive
来满这个需求。
例如,JavaFX用户界面元素的javafx.controls
模块。javafx.controls
模块需要javafx.base
模块,因此每个使用javafx.controls
的程序都需要javafx.base
模块。因此javafx.controls
模块声明需要使用transitive
修饰符:
1 | module javafx.controls |
任何声明需要javafx.controls
的模块现在都自动需要javafx.base
。
requires transitive
语句的一种很有吸引力的用法是聚集模块,即导入一个模块,自动导入可能需要的所有模块,无需再手动导入。
java.se
模块就是这样的模块,它被声明为下面的样子:
1 | module java.se |
对细颗粒度模块依赖不感兴趣的程序员可以直接声明需要java.se
,然后获取Java SE
平台的所有模块。
静态需求
requires static
声明了一种需求,它声明一个模块必须再编译时出现,而在运行时是可选的。下面是两个用例:
当问再编译时进行处理的注解,而该注解是在不同的模块中声明的。
对于位于不同模块中的类,如果它可用,就是用它,否则就执行其他操作。例如:
1
2
3
4
5
6try{
new oracle.jdbc.driver.OracleDriver();
...;
}catch(NoClassDefFoundError err){
Do something else;
}限定导出和开放
exports
和open
有一种变体用于将模块导出或开放给指定的模块,而不是所有的模块都可以访问。格式如下:
exports ... to ...
opens ... to ...
如:
- 限定导出
exports ... to ...
1
exports com.sun.java.javafx.collections to javafx.controls, javafx.graphics, javafx.fxml, javafx.swing;
则
com.sun.java.javafx.collections
包只能被javafx.controls, javafx.graphics, javafx.fxml, javafx.swing
访问。- 限定开放
opens ... to ...
1
2
3
4
5module priv.mw
{
requires com.xxx.yyy;
opens priv.mw.utils to priv.jack;
}
则priv.mw.utils
就只对priv.jack
模块开放。
服务加载
ServiceLoader
类提供了一种轻量级机制,用于将服务接口与现实匹配起来。下面是对服务加载的一个快速回顾。服务拥有一个接口和多个可能的接口。下面是一个简单的接口示例:
1 | public interface GreeterService{ |
有一个或多个模块提供了实现,例如:
1 | public class FrenchGreeter implements GreeterService{ |
服务消费者必须基于其认为合适的标准在提供的所有实现中选择一个:
1 | ServiceLoader<GreeterService> greeterLoader = ServiceLoader.load(GreeterService.class); |
在过去,实现是通过将文本放置到包含实现类的JAR文件的META-INF/services
目录中而提供给服务消费者。模块系统提供了一种更好的方式,与提供文本不同,可以添加语句刀到模块描述符中。
提供服务
服务提供者可以使用provides...with...
关键字,他列出了服务接口(可能定义在任何模块中),以及实现类(必须是该模块的一部分)。下面是jdk.security.auth
模块的一个例子:
1 | module jdk.security.auth |
这与META-INF/service
问价等价。
消费服务
服务使用者需要使用uses
关键词来使用服务,如:
1 | module java.base |
当消费模块中的代码调用ServiceLoader.load(ServiceInterface.class)
时,匹配的提供者类将被加载。
例如:将上面的GreeterService
提供了不地区的实现。
实现:
1 | moudle com.horstman.greetsvc |
消费模块声明消费该模块:
1 | module priv.mw |
调用:
1 | package priv.mw; |