Java 9 引入了新的包抽象等级,官方名称 Java Platform Module System (JPMS) ,简称模块化。
什么是模块
模块是一组密切相关的包和资源以及一个新的模块描述符文件。换句话说,模块是一组 java包(Java Packages) 的抽象,可以帮助我们提升代码复用度,清晰的反应各个模块之间的依赖关系。
其中包含的概念:
- Java Packages 即java包,原先就存在的概念。
- Resources 资源文件,每个模块需要为自己的资源负责,比如配置文件、图片等。原先我们是把这些资源文件统一维护在项目的根目录级别的,使用模块之后我们把资源文件放入相关的模块中维护。
- Module Descriptor 文件。描述模块的名称、版本、依赖等。
模块描述
模块描述符是描述模块各类信息的,主要包括:
- Name – 模块的名称
- Dependencies – 当前模块依赖的其他模块列表
- Public Packages – 我们希望从模块外部访问的所有包的列表
- Services Offered – 我们可以提供给其他模块消费的服务实现
- Services Consumed – 允许当前模块消费的服务
- Reflection Permissions – 明确的指名哪些包可以被其他模块反射调用
模块的命名规则类似java包的命名规则(使用.分隔),可以使用项目的命风格my.module,也可以使用 DNS倒序的风格com.baeldung.mymodule。
默认情况下模块中的所有的java包都是私有的,无法被外部访问,所以我们需要明确的列出哪些java包可以被外部访问,这里说明一下子包是不会继承父包属性的,子包和父包都需要分别导出 ,反射权限同理。
模块类型
模块分为四种类型:
System Modules系统模块,是JDK自带的模块,比如java.base、java.logging等。Application Modules应用模块,是我们构建的jar包中的模块,并通过编译后生成的module-info.class文件命名和定义的模块。Automatic Modules自动模块,通过模块路径(module-path)引用的 未定义模块的jar包,此时模块的名称可以通过jar包的名称确定。自动模块默认有权限访问所有其他模块的导出包(未导出的还是不能访问,原文Automatic modules will have full read access to every other module loaded by the path.交代的不清楚),且默认exported所有包。Unnamed Module未命名模块,通过class-path加载的jar包,自动被加入到未命名模块(注意这里原文说的也不清楚,无论是否声明模块化都会加入未命名模块)。
默认模块
java 9 对 jdk 本身做了模块化
可以使用
1 | java --list-modules |
查看jdk默认的模块。
模块声明
要启用模块,首先需要在包的根目录下创建一个名为module-info.java的文件。这个文件就是模块的描述,包含了所有构建与使用模块的内容。
1 | module my.module { |
模块的指令说明如下。
Requires
requires 指令声明了本模块的依赖:
1 | module my.module { |
此配置说明了不论在 运行时 或 编译时 本模块都依赖模块module.name,此时模块module.name中声明的所有导出java包都可以被我们的模块访问。
Requires Static
有时候我们的模块A依赖了另一个模块B,但是使用我们模块A的用户可能并不想引用模块B,这时候可以使用requires static指令:
1 | module my.module { |
此配置说明了在 编译时 本模块依赖模块module.name,使用模块A的其他模块并不会包含模块B。
Requires Transitive
当别人引用我们的模块的时候,也需要引用我们模块所引用的模块,否则我们的模块无法正常工作,此时,我们可以使用requires transitive 来实现此类模块依赖的传递性。
1 | module my.module { |
有了此配置,所有依赖我们my.module的都会自动依赖module.name模块,而不需要自己手动声明。
Exports
默认情况下一个模块不会对其他模块暴露任何API。这种强的封装的原则是创建 JPMS 的动机之一。
如果我们需要暴露我们的API给到其他使用,可以使用exports指令来暴露所有指定java包中的 public 的成员
1 | module my.module { |
此时如果其他人requires my.module 他们就可以(仅可以)访问java包com.my.package.name中的所有public类型的成员。
Exports … To
exports指令对所有人都暴露了API,如果我们只想对指定的模块暴露我们的API,可以使用exports ... to指令
1 | module my.module { |
效果和exports指令类似,但是只有对com.specific.module模块有效。
(这里原文写的是com.specific.package,写的不准确,导出应该是针对模块的而不是包的。)
Uses
服务(service)是接口或者抽象类的实现,可以提供给其他类消费。指定当前模块消费服务使用uses指令。
需要注意的是uses指令指定的是服务的接口或者抽象类,并不是具体的实现类。
1 | module my.module { |
这里说明一下requires指令和uses指令的区别,比如我们想消费一个其他模块A提供的服务,而这个服务接口的实现在模块B中。
此时如果我们使用requires指令,就必须感知到其他接口的实现模块并明确的requires进来,但如果使用uses指令只需要指定服务的接口就可以了。
Provides … With
模块可以提供服务给其他模块消费。Provides指令后跟提供服务的接口,with 关键字后跟实现服务接口的实现类:
1 | module my.module { |
Open
上文提到过模块设计的目标之一就是强封装性。在java 9之前,可以通过反射来访问包中的所有类型所有成员,甚至包括了私有变量,这样其实毫无封装可言。
java 9 为了实现强封装性,我们需要明确的授权才能使其他模块可以通过反射访问我们的类。
如果我们想和以前一样,所有模块都可以通过反射访问所有我们的类,我们可以使用open指令:
1 | open module my.module { |
Opens
如果我们想允许其他模块通过反射访问我们指定的私有类型,我们可以使用opens关键字来暴露对应的java包。
1 | module my.module { |
要注意的是,即使opens允许通过反射访问私有成员,反射代码仍然需要通过java.lang.reflect包中的方法显式地取消访问控制检查。
例如,如果你要通过反射访问私有字段,你通常需要使用Field.setAccessible(true)来允许访问私有变量。
Opens … To
跟进一步,我们可以通过opens to指令把我们的java包的反射访问权限授权给预先指定的模块。
1 | module my.module { |
命令行参数
这里有必要讲一下如何使用模块系统的命令行工具,我们再用下面的命令行参数来巩固一下整个模块系统的知识。
- module-path 指定模块路径,这里可以指定包含模块的目录,多个目录的分隔符:windows系统使用
;,linux系统使用:。 - add-reads 指定模块的依赖关系,同模块声明文件中的
requires指令。 - add-exports 指定模块的导出关系,同模块声明文件中的
exports指令。 - add-opens 指定模块的可反射访问的java包,同模块声明文件中的
opens指令。 - add-modules 添加指定的模块列表进入默认的模块集(默认加载的模块)。
- list-modules 列出默认的模块集。
- patch-module 添加或重载模块中的类。
- illegal-access=permit|warn|deny 配置非法访问的处理方式,
permit允许访问且不警告(默认),warn警告但允许访问,deny拒绝访问。
可见性
很多java 库都是通过反射来实现的,在java 9中默认只能访问导出的public类型,即使我们使用了setAccessible(true)也无法访问非public类型的成员。
我们可以使用open, opens, opens to来允许运行时的反射访问,但也仅限于运行时。编译时仍然是无法访问私有类型的,当然也不该去访问。
如果我们一定要通过反射访问没有open的模块成员,我们可以使用命令行–add-opens参数来添加反射访问权限。
总结
这篇文章讲了 java 9 模块系统的基础概念,以及如何使用。可以参考 Github上的演示代码