JDK9 引入了模块,用于描述代码之间的关系和依赖,控制某模块是否可以被其他模块访问。
模块基础
一个模块由一组包和资源组成。模块定义在名为 module-info.java 的文件中,javac 将这个文件编译成类文件,称为 module descriptor。该文件仅能包含一个模块定义。
模块定义的一般形式为:
// name 为模块名,为单个有效标识符或者一组以句号(.)分隔的标识符
module name {
// 定义,可为空
}
一个模块可以指定它所依赖的模块,使用 requires
关键字,这个依赖关系在编译和运行时检查;一个模块可以控制它的包能否被其他模块访问,需要使用 exports
关键字,包中 public 和 protected 类型只有显式导出才能被其他模块访问。
requires module
: 在模块定义中指定了当前模块依赖的模块 module,这条语句执行时,如果 module 模块不存在,则无法编译。可以定义多条该语句。
exports package
: 在模块定义中,指定了当前模块导出的包。这条语句执行时,导出当前模块中 package 包。可以定义多条该语句。
模块导出一个包时,该包中所有 public 和 protected 类型都可以被其他模块访问。没有导出的包,里面任何类型在该模块外访问不到。包中 public 和 protected 类型不论是否导出,在包所在的模块中都可以访问。
在依赖关系中,requires
和 exports
都需要用到。对于依赖模块,必须使用 requires
指明它所依赖的模块,被依赖的模块必须使用 exports
导出依赖模块需要的包。无论哪一方缺失相关的语句,都将不能通过编译。
requires
和 exports
必须在模块定义中使用,模块定义必须位于名为 module-info.java 的文件中。
java.base 和 platform modules
API 模块称为 platform modules,这些模块都以“java” 作为前缀,如 java.base、java.desktop、java.xml 等。通过将 API 模块化,应用只需要它用到的 API 模块,而不需要整个 JRE,显著减小体积。
API 模块中最重要的是 java.base 模块,里面包含和导出了所有常用的包,如 java.lang、java.io、java.util 等。其他所有模块都可以访问该模块,在模块定义中不需要显式声明 requires java.base
,默认添加了。
从 JDK9 开始,Java 文档中写明了包所在的模块。如果该包位于 java.base 中,可以直接使用该包中定义的内容,否则需要使用 requires
声明依赖关系才能使用。
遗留代码和未命名模块
当使用的代码不在命名模块中,则该代码自动属于未命名模块,未命名模块自动导出所有包,同时未命名模块可以访问其他所有模块。
当编译不使用模块的程序时,使用类路径而不是模块路径。
以上两种特性保证了遗留代码(模块使用之前的代码)和使用模块的代码之间的兼容性。
大项目使用模块可以获得好处,如控制访问性;简单程序基本不需要使用模块。
针对特定模块导出
有时候并不想将一个模块中的包导出给所有其他模块使用,这个时候可以使用限定导出(qualified export),一般形式为:exports pkg to modules
。其中,modules 指一个或用逗号(,)分隔的一组模块,这些模块可以使用导出的 pkg 包。
requires 传递
当 A requires B,B requires C 时,如果 A 模块既需要 B 导出的包,又需要 C 导出的包,可以声明 A requires C 解决该问题。但是如果类似这种情况很多,这种做法显得冗余。另一种做法称为 implied readability,即在 B 中添加 requres transitive C
。这条语句表明任何依赖 B 的语句都自动依赖 C。
transitive 如果后面紧跟一个分隔符会被认为是一个标识符。
使用服务
通过使用插件的方式,可以在不改变应用核心的情况下增强应用的功能。Java 通过 services 和 service providers 提供 pluggable 应用架构。模块系统对此提供了支持。
Java 中,service 是由接口或抽象类定义功能的程序单元,而具体的实现由 service provider 提供。service 提供了 pluggable 架构,例如 service 提供语言的翻译功能,而从某个具体语言翻译成另外一种具体语言由 service provider 实现。
class ServiceLoader<S>
支持 service provider,S 指服务类型。service provider 通过 load()
方法加载,形式之一为:public static <S> ServiceLoader<S> load(Class<S> serviceType)
。调用 load()
方法时,返回 ServiceLoader 的实例,该实例支持迭代和循环遍历。
模块使用 provides
语句指定它提供的服务,使用 use
语句指定它需要的服务,同时使用 with
指定需要的 service provider 类型。一般形式为:
// 指定模块提供的服务类型(一般是接口,抽象类也可以)
// implements 是由逗号分隔的多个具体实现
provides serviceType with implements;
// 指定该模块需要的服务类型
use serviceType;
模块图
编译器在解析模块之间的依赖关系时会创建模块图表示依赖,模块图将表示所有相关模块的依赖关系。java.base 不包括在图中,因为自动依赖。
三个模块特性
open 模块的所有包在运行时可以被其他模块访问,无论是否导出。主要用于反射。显式导出的包在编译时可以被其他模块访问,未导出的不能。open 模块定义的一般形式为:
open module Name {
// 定义
}
open 语句用于指定模块中特定的包可以在运行时被其他所有模块访问,与编译时的访问性无关。open 语句不能在 open 模块中使用,可以使用 to 子句限定允许访问该包的模块。open 语句的一般形式为: open pkg
。
requires 语句指定编译时和运行时的依赖关系,如果在该语句中使用静态修饰符将指定编译时的依赖关系,不指定运行时的依赖关系。一般形式为:requires static module
。
jlink 和 模块 jar 文件
JDK9 提供了 jlink 工具,该工具将一组模块打包成运行时 image,在应用根目录的上一级目录中运行
jlink --launcher appName=url/pkg.mainClass
--module-path javaHome\jmods;root\modules
--add-modules startSubDir
--output outputDir
得到打包成的运行时 image。
层和自动化
在模块图中使用同一个类加载器的模块构成模块层(module layers)。不同层使用不同的类加载器。
通过在模块路径中指定 nonmodular JAR 文件可以创建自动模块,自动模块的名字自动获得,也可以在 manifest 文件中指定。正常模块可以依赖自动模块中的代码。
参考
[1] Herbert Schildt, Java The Complete Reference 11th, 2019.