Java平台模块系统- 模块声明
在提到Java 9时,最重要的话题是Project Jigsaw,也就是Java平台模块系统(Java Platform Module System,JPMS)。JPMS把模块化引入了Java平台中。Project Jigsaw本来计划作为Java 8的一部分,但是由于所涉及的改动过大,因此推迟到了Java 9中。模块系统不仅给Java平台本身带来了巨大的改动,也给在Java平台上运行的应用程序带来了革命性的变化。
在Java 9中,Java SE平台和JDK本身都以模块化的方式来组织,因此在经过缩减之后可以运行在小型设备上。在Java 9 之前,JDK和JRE的安装是一个不开切分的整体。JRE中包含了不同的应用程序所需要使用的各种工具和标准库。但对于一个特定的应用来说,在绝大多数情况下都只会用到其中一部分的工具和标准库。这就意味着JRE中的部分内容其实是完全多余的。举例来说,一个API代理程序很可能永远都用不到与用户界面相关的AWT/Swing库。在完成Java平台本身的模块化之后,开发人员就可以通过移除不必要的模块的方式来为应用打造专属的JRE,而只保留该应用所需要的模块。这可以极大的减少应用程序安装包的大小,从而节省存储空间和网络传输带宽。
Java社区一直以来都希望有一种方式来构建模块化Java应用。OSGi是目前比较流行的一个选择。Project Jigsaw同样可以让开发人员构建模块化的Java库和应用。相较于OSGi而言,Java平台自身提供的模块化实现显然更有吸引力。
Project Jigsaw本身是一个非常复杂的实现,其基本内容定义在JSR 376: JavaTM Platform Module System中,还有与之对应的6个JEP。
- 200:模块化JDK
- 201:模块化源代码
- 220:模块化运行时镜像
- 260:封装大部分内部API
- 261:模块系统
- 282:jlink – Java链接器
本章中包含与JPMS相关的重要内容。
模块概述
根据Oracle的Java平台集团的首席架构师Mark Reinhold在一篇文章中的论述:
模块是一个命名的,自我描述的代码和数据的集合。模块的代码被组织成多个包,每个包中包含Java类和接口;模块的数据则包括资源文件和其他静态信息。
从上述对模块的定义可以知道,模块只是按照预先定义的结构来进行组织的编译后的Java代码。如果你已经使用Maven的多模块功能,或是Gradle的多项目功能,那么每个Maven模块或Gradle项目都可以很容易地转换成JPMS模块。
每个模块都需要有一个名字。模块应该遵循与Java包同样的命名惯例,也就是翻转域名模式,如com.mycompany.mymodule
。
一个JPMS模块通过根目录中的module-info.java
文件来描述。该文件被编译成module-info.class
。在这个文件中,我们使用新的关键词module
来声明一个模块。下面的代码中给出了模块com.mycompany.mymodule
所对应的module-info.java
文件的内容。该文件只是声明了一个模块,并没有对它进行具体的描述。相关的内容会在后续的小节中提到。
module com.mycompany.mymodule {
}
示例应用
为了更好地说明模块系统的使用方式,本章中会使用一个简单的电子商务应用来作为示例。该应用只提供了非常有限的功能,其主要目的是为了展示模块之间的依赖关系。该应用的名称空间是io.vividcode.store
。下表给出了示例应用中的模块。表中名字为common
的模块,其实际的名称是io.vividcode.store.common
。
common
– 通用APIcommon.persistence
– 通用持久化APIfilestore
– 基于文件的持久化实现product
– 产品APIproduct.persistence
– 产品持久化实现runtime
– 应用启动
模块声明
模块声明文件module-info.java
是了解模块系统的第一步。
requires和exports
在引入了模块系统之后,Java应用程序应该被组织成不同的模块。每个模块可以通过requires
来声明其对其他模块的依赖关系。依赖一个模块并不意味着就自动获得了访问该模块中所包含的Java类型的许可。一个模块可以声明其中所包含的哪些包是可供其他模块访问的。只有被导出的包才能被其他模块所访问。而在默认情况下,是没有任何包会被导出的。我们可以通过exports
声明来导出包。导出的包中包含的public
和protected
类型,以及这些类型中包含的public
和protected
成员是可以被其他模块访问的。
下面的代码中给出了模块io.vividcode.store.common.persistence
的module-info.java
文件的内容。该文件使用了两个requires
声明来声明该模块对模块slf4j.api
和io.vividcode.store.common
的依赖关系。模块slf4j.api
由SLF4J库提供,而模块io.vividcode.store.common
则是项目中的另外一个模块。模块io.vividcode.store.common.persistence
导出了其中的包io.vividcode.store.common.persistence
。
module io.vividcode.store.common.persistence {
requires slf4j.api;
requires io.vividcode.store.common;
exports io.vividcode.store.common.persistence;
}
需要注意的是,当导出一个包时,只有该包中的类型会被导出,子包中的类型不会被自动导出。如果声明导出的包为com.mycompany.mymodule
,类似com.mycompany.mymodule.A
和com.mycompany.mymodule.B
这样的类型会被导出。而类似com.mycompany.mymodule.impl.C
和com.mycompany.mymodule.test.demo.D
这样的类型则不会。如果需要导出子包,必须使用exports
来对每个子包进行显式声明。
如果一个模块中的类型不能被其他模块所访问,那么该类型等同于该模块中的私有类型或类型中的私有成员。试图使用这些类型或成员会产生编译错误。在运行时则会由JVM抛出java.lang.IllegalAccessError
错误。如果试图通过Java反射API来访问,则会抛出java.lang.IllegalAccessException
异常。
所有的模块,除了java.base
模块本身,都有一个隐式的和强制的对于java.base
模块的依赖关系。你不需要在module-info.java
文件中进行声明。模块java.se
中包含了Java SE的核心包,如java.lang
等。
传递依赖
当模块A依赖模块B时,模块A可以访问模块B中导出的public
和protected
类型。我们把这种关系称为模块A读取(read)模块B。同理,如果模块B读取模块C,模块B也可以访问模块C导出的public
和protected
类型。也就是说,模块B可以在其包含的代码中,使用模块C中的类型来作为方法的参数或是返回类型。下面的代码给出了模块C的module-info.java
文件。模块C导出了包ctest
。
module C {
exports ctest;
}
下面的代码给出了模块C中的类ctest.MyC
。其中的方法sayHi
用来在控制台打印出一条消息。
package ctest;
public class MyC {
public void sayHi() {
System.out.println("Hi from module C!");
}
}
下面的代码给出了模块B的module-info.java
文件。模块B通过requires
声明了对模块C的依赖,同时导出了包btest
。
module B {
requires C;
exports btest;
}
下面的代码给出了类btest.MyB
的getC
方法,该方法返回一个类MyC
的新实例。
package btest;
import ctest.MyC;
public class MyB {
public MyC getC() {
return new MyC();
}
}
在模块A的module-info.java
文件中,只声明了对于模块B的依赖关系。
module A {
requires B;
}
我们可以在模块A中的类atest.MyA
中使用类MyC
,如下面的代码所示。
package atest;
import btest.MyB;
public class MyA {
public static void main(String[] args) {
new MyB().getC().sayHi();
}
}
当我们尝试去编译上面的代码时,会发现出现如下的编译错误。这是因为模块A在其module-info.java
中并没有声明对模块C的依赖关系,因此模块A并没有读取模块C。模块的读取关系默认并不是传递的。
/<code_path>/A/atest/MyA.java:7: error: MyC.sayHi() in package ctest is not
accessible
new MyB().getC().sayHi();
^
(package ctest is declared in module C, but module A does not read it)
1 error
为了使得类atest.MyA
通过编译,我们可以在其module-info.java
文件中添加“requires C;”
来声明对模块C的依赖关系。这种方式如果要应用在所有可能的通过传递关系产生的依赖时,手动添加这些依赖声明是一件繁琐的工作。由于传递依赖关系是一个常见的需求,Java 9提供了专门的方式来处理这种情况。requires
可以添加一个新的描述符transitive
来声明一个依赖关系是传递的。一个模块中声明为可传递的依赖模块,可以被依赖该模块的其他模块来读取。这种读取关系成为隐式可读性(implicit readability)。对于上面的例子,只需要把模块B对模块C的依赖关系声明为可传递即可。这样模块B的可传递依赖模块C,就可以被依赖模块B的模块A所读取,从而模块A的代码可以被成功编译。
module B {
requires transitive C;
exports btest;
}
一般来说,对于一个模块所导出的Java类型来说,如果其中的方法型构中引用了来自另外一个模块的类型,那么在当前模块的module-info.java
文件中,对于该模块的依赖声明应该使用requires transitive
而不是requires
。正如同在上面的例子中,类MyB
中的方法getC
的返回值类型是模块C中的类MyC
,因此模块B的声明中应该使用requires transitive C
,而不是requires C
。
静态依赖
静态依赖是一种特殊的依赖关系,通过requires static
来进行声明。静态依赖所声明的模块在编译时是必须的,但是在运行时是可选的。
module demo {
requires static A;
}
静态依赖对于框架和第三方库来说比较实用。假设我们需要开发一个可以和不同数据库交互的库。这个库所在的模块可以使用静态依赖来声明对所支持的数据库驱动的依赖关系。在编译时,库中的代码可以访问这些驱动中的类型;在运行时,用户只需要添加所需要使用的驱动即可。如果不使用静态依赖,用户必须要添加所有支持的驱动才能完成模块的解析。
服务
Java平台有自己的服务接口和服务提供者机制。这是通过类java.util.ServiceLoader来完成服务提供者的查找。服务机制主要用在JDK本身以及第三方框架和库中。服务机制的一个典型应用是JDBC驱动。每个JDBC驱动都需要提供服务接口java.sql.Driver
的实现。驱动的JAR文件的META-INF/services
目录中需要包含一个名为java.sql.Driver
的文件。比如,Apache Derby的JAR文件中的java.sql.Driver
文件的内容如下所示。其中org.apache.derby.jdbc.AutoloadedDriver
是服务接口java.sql.Driver
的实现类的名称。
org.apache.derby.jdbc.AutoloadedDriver
在Java 9之前,ServiceLoader
通过扫描CLASSPATH来查找特定服务接口的实现类。在Java 9中,模块成了代码的组织单元。模块声明文件module-info.java
提供了与服务使用者和提供者相关的声明。
下面的代码给出了模块io.vividcode.store.common
中的服务接口PersistenceService
的声明。
package io.vividcode.store.common;
public interface PersistenceService {
void save(final Persistable persistable) throws PersistenceException;
}
该服务接口被模块io.vividcode.store.common.persistence
所使用。在该模块的module-info.java
文件中,我们通过关键词uses
来声明对服务接口io.vividcode.store.common.PersistenceService
的使用。
module io.vividcode.store.common.persistence {
requires slf4j.api;
requires transitive io.vividcode.store.common;
exports io.vividcode.store.common.persistence;
uses io.vividcode.store.common.PersistenceService;
}
在修改了模块描述文件之后,可以通过ServiceLoader
来查找该服务接口的提供者,并使用该接口来完成所需功能。对于一个服务接口,可能有多个服务提供者实现。ServiceLoader
的load()
方法返回是一个Stream
对象。这里我们通过findFirst()
方法来获取第一个实现。然后使用PersistenceService
接口的save()
方法来保存对象。
public class DataStore<T extends Persistable> {
private final Optional<PersistenceService> persistenceServices;
public DataStore() {
this.persistenceServices = ServiceLoader
.load(PersistenceService.class)
.findFirst();
}
public void save(final T object) throws PersistenceException {
if (this.persistenceServices.isPresent()) {
this.persistenceServices.get().save(object);
}
}
}
服务接口io.vividcode.store.common.PersistenceService
的提供者在模块io.vividcode.store.filestore
中,是一个基于文件系统的持久化实现。下面的代码给出了模块io.vividcode.store.filestore
的module-info.java
文件。
provides io.vividcode.store.common.PersistenceService with io.vividcode.store.filestore.FileStore
的含义是该模块提供了服务接口io.vividcode.store.common.PersistenceService
的实现类io.vividcode.store.filestore.FileStore
。
受限导出
当一个模块的声明中使用exports
来导出一个包时,所有其他通过requires
来声明对其依赖关系的模块都可以访问此包中的类型。在某些情况下,我们会希望可以限制某些包对于其他模块的可见性。举例来说,一个包可能在最早的设计中是对所有模块都公开的,但是该包在后来的版本更新中被新的包所替代,因此被声明为废弃的。这个被废弃的包应该只能被遗留代码所使用。在升级到Java 9的模块系统之后,包含该包的模块应该只是把该包导出给还使用遗留代码的模块。这样可以确保遗留代码不会被继续使用。通过在exports
声明后添加to
语句,可以指定允许访问该包的模块名称。
下面的代码给出了JDK模块java.rmi
的描述文件。从中可以看到包com.sun.rmi.rmid
只对模块java.base
可见,而包sun.rmi.server
则只对模块jdk.management.agent
、jdk.jconsole
和java.management.rmi
可见。
module java.rmi {
requires java.logging;
exports java.rmi.activation;
exports com.sun.rmi.rmid to java.base;
exports sun.rmi.server to jdk.management.agent,
jdk.jconsole, java.management.rmi;
exports javax.rmi.ssl;
exports java.rmi.dgc;
exports sun.rmi.transport to jdk.management.agent,
jdk.jconsole, java.management.rmi;
exports java.rmi.server;
exports sun.rmi.registry to jdk.management.agent;
exports java.rmi.registry;
exports java.rmi;
uses java.rmi.server.RMIClassLoaderSpi;
}
开放模块和包
在模块声明文件中,可以在module
之前添加open
描述符来把该模块声明为开放的。一个开放的模块在编译时只允许其他模块访问其通过exports
声明来显式导出的包。而在运行时,模块中的所有包都是被导出的,包括那些没有通过exports
声明的包。同样的,也可以通过Java反射API来访问所有包中的所有Java类型。所有Java类型中包括私有类及其私有成员。如果使用Java反射API来绕开Java语言的访问检查机制,如AccessibleObject
类的setAccessible()
方法,就可以访问开发模块中的私有类型和成员。
对于每个具体的包,也可以使用opens
来把它声明为开放的。开放的包可以通过Java反射API来访问。就如同开放模块一样,使用反射API可以访问开发包中的所有类型及其所有成员。开放包的声明也支持通过to
语句来指定可访问的模块名称。下面的代码给出了一个开放模块E的描述文件。
open module E {
exports etest;
}
下面的代码则给出了使用开放包的模块声明文件。模块F中声明了两个包是开放的。如果所声明开放的包在模块中不存在,编译器会给出警告;同样的,如果开放模块所限制访问的目标模块不存在,编译器也会给出警告。
module F {
opens ftest1;
opens ftest2 to G;
}
开放模块和开放包的主要作用是解决向后兼容性的问题。在迁移使用反射API的遗留代码时,可能会需要用到它们。