Java 9中模块化简介
1. 概述
Java 9 在包之上引入了一个新的抽象级别,正式称为 Java 平台模块系统 (JPMS),或简称为“模块”。 在本教程中,我们将介绍新系统并讨论其各个方面。 我们还将构建一个简单的项目来演示我们将在本指南中学习的所有概念。
2. 什么是模块?
首先,我们需要了解什么是模块,然后才能了解如何使用它们。 模块是一组密切相关的包和资源以及一个新的模块描述符文件。
换句话说,它是一个“Java 包的包”抽象,它允许我们使我们的代码更加可重用。
2.1. 套餐
模块中的包与我们自 Java 诞生以来一直使用的 Java 包相同。 当我们创建一个模块时,我们在内部将代码组织在包中,就像我们之前对任何其他项目所做的那样。 除了组织我们的代码外,包还用于确定哪些代码可以在模块之外公开访问。我们将在本文后面花更多时间讨论这个问题。
2.2. 资源
每个模块负责其资源,如媒体或配置文件。
以前,我们将所有资源放入项目的根级别,并手动管理哪些资源属于应用程序的不同部分。 使用模块,我们可以将所需的图像和 XML 文件与需要它的模块一起发送,从而使我们的项目更易于管理。
2.3. 模块描述符
当我们创建一个模块时,我们包含一个描述符文件,它定义了我们新模块的几个方面:
- Name – 我们模块的名称
- Dependencies – 此模块所依赖的其他模块的列表
- Public Packages——我们希望从模块外部访问的所有包的列表
- Services Offered——我们可以提供其他模块可以使用的服务实现
- Services Consumed – 允许当前模块成为服务的消费者
- Reflection Permissions——明确允许其他类使用反射来访问包的私有成员
模块命名规则类似于我们命名包的方式(允许使用圆点,不允许使用破折号)。使用项目样式 (my.module) 或反向 DNS ( com.blogdemo.mymodule ) 样式名称是很常见的。我们将在本指南中使用项目风格。
我们需要列出我们想要公开的所有包,因为默认情况下所有包都是模块私有的。
反射也是如此。默认情况下,我们不能对从另一个模块导入的类使用反射。 在本文后面,我们将查看如何使用模块描述符文件的示例。
2.4. 模块类型
新的模块系统中有四种类型的模块:
- 系统模块——这些是我们在上面运行 list-modules命令时列出的模块。它们包括 Java SE 和 JDK 模块。
- 应用程序模块——这些模块是我们在决定使用模块时通常想要构建的。它们在 包含在组装 JAR中的编译后的module-info.class文件中命名和定义。
- 自动模块——我们可以通过将现有的 JAR 文件添加到模块路径来包含非官方模块。模块的名称将派生自 JAR 的名称。自动模块将对路径加载的每个其他模块具有完全读取权限。
- 未命名模块 – 当类或 JAR 加载到类路径而不是模块路径时,它会自动添加到未命名模块。它是一个包罗万象的模块,可保持与以前编写的 Java 代码的向后兼容性。
2.5. 分配
模块可以通过以下两种方式之一分发:作为 JAR 文件或作为“分解”的编译项目。当然,这与任何其他 Java 项目相同,因此也就不足为奇了。 我们可以创建由“主应用程序”和几个库模块组成的多模块项目。 但是我们必须小心,因为每个 JAR 文件只能有一个模块。 当我们设置构建文件时,我们需要确保将项目中的每个模块捆绑为一个单独的 jar。
3. 默认模块
当我们安装 Java 9 时,我们可以看到 JDK 现在有了新的结构。
他们已将所有原始软件包移至新的模块系统中。 我们可以通过在命令行中输入来查看这些模块是什么:
java --list-modules
这些模块分为四个主要组: java、javafx、jdk和 Oracle。 java模块是核心 SE 语言规范的实现类。 javafx 模块是 FX UI 库。
JDK 本身需要的任何东西都保存在 jdk模块中。 最后,Oracle 特定的任何东西都在 oracle模块中。
4. 模块声明
要设置模块,我们需要在包的根目录下放置一个名为 module-info.java的特殊文件。
该文件称为模块描述符,包含构建和使用我们的新模块所需的所有数据。 我们用一个声明来构造模块,它的主体要么是空的,要么由模块指令组成:
module myModuleName {
// all directives are optional
}
我们以module关键字开始模块声明,然后是模块的名称。 该模块将使用此声明,但我们通常需要更多信息。 这就是模块指令的用武之地。
4.1. requires
我们的第一个指令是 requires。这个模块指令允许我们声明模块依赖:
module my.module {
requires module.name;
}
现在,my.module 对module.name有 运行时和编译时依赖。 当我们使用这个指令时,我们的模块可以访问从依赖项导出的所有公共类型。
4.2. requires静态
有时我们编写的代码引用了另一个模块,但我们库的用户永远不想使用。 例如,我们可以编写一个实用函数,当另一个日志模块存在时,它会漂亮地打印我们的内部状态。但是,并不是我们库的每个消费者都想要这个功能,他们也不想包含一个额外的日志库。 在这些情况下,我们希望使用可选依赖项。通过使用 requires 静态指令,我们创建了一个仅编译时的依赖项:
module my.module {
requires static module.name;
}
4.3. requires传递
我们通常与图书馆合作,以使我们的生活更轻松。 但是,我们需要确保任何引入我们代码的模块也会引入这些额外的“传递”依赖项,否则它们将无法工作。 幸运的是,我们可以使用 requires 传递指令来强制任何下游消费者也读取我们所需的依赖项:
module my.module {
requires transitive module.name;
}
现在,当开发人员requires my.module时,他们也不必说 requires module.name以使我们的模块仍然工作。
4.4. export
**默认情况下,一个模块不会将其任何 API 暴露给其他模块。**这种 强大的封装是最初创建模块系统的关键动力之一。
我们的代码明显更加安全,但是如果我们希望 API 可用,现在我们需要明确地向全世界开放我们的 API。 我们使用 export指令来公开命名包的所有公共成员:
module my.module {
exports com.my.package.name;
}
现在,当有人确实 requires my.module时,他们将可以访问我们的 com.my.package.name包中的公共类型,但不能访问任何其他包。
4.5. exports…to
我们可以使用 exports…to世界开放我们的公共课程。
但是,如果我们不希望全世界都访问我们的 API 怎么办? 我们可以使用exports…to 指令来限制哪些模块可以访问我们的 API 。
与 export 指令类似 ,我们将包声明为已导出。但是,我们还列出了我们允许哪些模块将此包作为requires导入。让我们看看它是什么样子的:
module my.module {
export com.my.package.name to com.specific.package;
}
4.6. 用途
service是可以被其他类使用 的特定接口或抽象类的实现。
我们使用 uses指令指定我们的模块使用的服务 。
请注意,我们 uses的类名是服务的接口或抽象类,而不是实现类:
module my.module {
uses class.name;
}
这里我们应该注意, requires指令和 uses指令之间是有区别的 。 我们可能requires一个模块来提供我们想要使用的服务,但该服务从它的传递依赖项之一实现了一个接口。 为了以防万一,我们 没有强制我们的模块需要所有传递依赖项,而是使用uses指令将所需的接口添加到模块路径中。
4.7. Provides … With
一个模块也可以是其他模块可以使用的service provider。 该指令的第一部分是 provides关键字。这是我们放置接口或抽象类名称的地方。 接下来,我们有 with指令,我们在其中提供实现类名称,该类名称要么implements接口 ,要么extends抽象类。
这是放在一起的样子:
module my.module {
provides MyInterface with MyInterfaceImpl;
}
4.8. open
我们之前提到,封装是设计这个模块系统的驱动力。 在 Java 9 之前,可以使用反射来检查包中的每个类型和成员,甚至是 *private *的。没有什么是真正封装的,这会给库的开发者带来各种各样的问题。 因为 Java 9 强制执行 强封装,所以我们现在必须显式授予其他模块的权限以反映我们的类。 如果我们想继续像旧版本的 Java 那样允许完全反射,我们可以简单地 open整个模块:
open module my.module {
}
4.9. opens
如果我们需要允许私有类型的反射,但我们不想暴露所有代码,我们可以使用 opens指令来暴露特定的包。 但请记住,这将向整个世界打开包,因此请确保这是您想要的:
module my.module {
opens com.my.package;
}
4.10. Opens … To
好的,所以反射有时很棒,但我们仍然希望从 encapsulation中获得尽可能多的安全性。我们可以选择性地将我们的包打开到预先批准的模块列表中,在这种情况下,使用 opens…to 指令:
module my.module {
opens com.my.package to moduleOne, moduleTwo, etc.;
}
5.命令行选项
到目前为止,Maven 和 Gradle 已经添加了对 Java 9 模块的支持,因此您无需手动构建项目。但是,了解如何从命令行使用模块系统仍然很有价值 。 我们将在下面的完整示例中使用命令行来帮助巩固整个系统在我们脑海中的工作方式。
- module-path **–**我们使用 –module-path选项来指定模块路径。这是包含您的模块的一个或多个目录的列表。
- add-reads – 我们可以使用与requires指令等效的命令行,而不是依赖模块声明文件 ; –添加读取。
- add-exports **–**导出指令的命令行替换。
- add-opens *–*替换模块声明文件中的 open子句。
- add-modules *–*将模块列表添加到默认模块集中
- list-modules *–*打印所有模块及其版本字符串的列表
- patch-module – 添加或覆盖模块中的类
- illegal-access=permit|warn|deny——要么通过显示单个全局警告来放松强封装,要么显示每个警告,要么因错误而失败。默认值为 permit。
6. 能见度
我们应该花一点时间来谈谈我们代码的可见性。 许多库依靠反射来发挥它们的魔力(想到 JUnit 和 Spring)。 默认情况下,在 Java 9 中,我们 只能访问导出包中的公共类、方法和字段。即使我们使用反射来访问非公共成员并调用 *setAccessible(true),*我们也无法访问这些成员。 我们可以使用 open、 opens和 opens…to选项来授予仅限运行时的反射访问权限。请注意,这仅适用于运行时! 我们将无法针对私有类型进行编译,而且我们永远不需要这样做。 如果我们必须访问一个模块进行反射,并且我们不是该模块的所有者(即,我们不能使用 opens…to指令),那么可以使用命令行 –add-opens选项来允许自己的模块在运行时反射访问锁定的模块。
唯一需要注意的是,您需要访问用于运行模块的命令行参数才能使其工作。
7. 把它们放在一起
现在我们知道了模块是什么以及如何使用它们,让我们继续构建一个简单的项目来演示我们刚刚学到的所有概念。 为简单起见,我们不会使用 Maven 或 Gradle。相反,我们将依靠命令行工具来构建我们的模块。
7.1. 设置我们的项目
首先,我们需要设置我们的项目结构。我们将创建几个目录来组织我们的文件。 首先创建项目文件夹:
mkdir module-project
cd module-project
这是我们整个项目的基础,所以在这里添加文件,例如 Maven 或 Gradle 构建文件、其他源目录和资源。 我们还放置了一个目录来保存我们所有的项目特定模块。 接下来,我们创建一个模块目录:
mkdir simple-modules
以下是我们的项目结构:
module-project
|- // src if we use the default package
|- // build files also go at this level
|- simple-modules
|- hello.modules
|- com
|- blogdemo
|- modules
|- hello
|- main.app
|- com
|- blogdemo
|- modules
|- main
7.2. 我们的第一个模块
现在我们已经有了基本的结构,让我们添加我们的第一个模块。 在 simple-modules目录下,创建一个名为 hello.modules的新目录。 我们可以为它命名任何我们想要的名称,但要遵循包命名规则(即,分隔单词的句点等)。如果需要,我们甚至可以使用主包的名称作为模块名称,但通常,我们希望使用与创建此模块的 JAR 相同的名称。 在我们的新模块下,我们可以创建我们想要的包。在我们的例子中,我们将创建一个包结构:
com.blogdemo.modules.hello
接下来,在这个包中创建一个名为 *HelloModules.java的新类。*我们将保持代码简单:
package com.blogdemo.modules.hello;
public class HelloModules {
public static void doSomething() {
System.out.println("Hello, Modules!");
}
}
最后,在 hello.modules根目录中,添加我们的模块描述符; 模块信息.java:
module hello.modules {
exports com.blogdemo.modules.hello;
}
为了简单起见,我们所做的只是导出 com.blogdemo.modules.hello包的所有公共成员。
7.3. 我们的第二个模块
我们的第一个模块很棒,但它什么也没做。 我们现在可以创建第二个使用它的模块。 在我们的 simple-modules目录下,创建另一个名为 main.app的模块目录。这次我们将从模块描述符开始:
module main.app {
requires hello.modules;
}
我们不需要向外界暴露任何东西。相反,我们需要做的只是依赖于我们的第一个模块,因此我们可以访问它导出的公共类。 现在我们可以创建一个使用它的应用程序。 创建一个新的包结构: com.blogdemo.modules.main。 现在,创建一个名为 MainApp.java 的新类文件。
package com.blogdemo.modules.main;
import com.blogdemo.modules.hello.HelloModules;
public class MainApp {
public static void main(String[] args) {
HelloModules.doSomething();
}
}
这就是我们演示模块所需的所有代码。我们的下一步是从命令行构建和运行此代码。
7.4. 构建我们的模块
要构建我们的项目,我们可以创建一个简单的 bash 脚本并将其放在项目的根目录中。 创建一个名为 compile-simple-modules.sh的文件:
#!/usr/bin/env bash
javac -d outDir --module-source-path simple-modules $(find simple-modules -name "*.java")
该命令有两个部分, javac和 find命令。 find命令只是输出所有. 我们的 simple-modules 目录下的java文件。然后我们可以将该列表直接提供给 Java 编译器。 与旧版本的 Java 不同,我们唯一要做的就是提供一个 module-source-path参数来通知编译器它正在构建模块。 运行此命令后,我们将拥有一个 outDir文件夹,其中包含两个已编译的模块。
7.5. 运行我们的代码
现在我们终于可以运行我们的代码来验证模块是否正常工作。 在项目的根目录中创建另一个文件: run-simple-module-app.sh。
#!/usr/bin/env bash
java --module-path outDir -m main.app/com.blogdemo.modules.main.MainApp
要运行一个模块,我们必须至少提供 模块路径和主类。如果一切正常,您应该看到:
>$ ./run-simple-module-app.sh
Hello, Modules!
7.6. 添加服务
现在我们对如何构建模块有了基本的了解,让我们让它变得更复杂一些。 我们将看到如何使用 provide…with和 uses指令。 首先在 hello.modules模块中定义一个名为 HelloInterface .java的新文件:
public interface HelloInterface {
void sayHello();
}
为了简单起见,我们将使用现有的 HelloModules.java类来实现这个接口:
public class HelloModules implements HelloInterface {
public static void doSomething() {
System.out.println("Hello, Modules!");
}
public void sayHello() {
System.out.println("Hello!");
}
}
这就是我们创建服务所需要做的一切 。 现在,我们需要告诉全世界我们的模块提供了这项服务。 将以下内容添加到我们的 module-info.java 中:
provides com.blogdemo.modules.hello.HelloInterface with com.blogdemo.modules.hello.HelloModules;
正如我们所看到的,我们声明了接口以及哪个类实现了它。 接下来,我们需要使用这个服务。在我们的 main.app模块中,让我们将以下内容添加到我们的 module-info.java中:
uses com.blogdemo.modules.hello.HelloInterface;
最后,在我们的 main 方法中,我们可以通过ServiceLoader 使用这个服务:
Iterable<HelloInterface> services = ServiceLoader.load(HelloInterface.class);
HelloInterface service = services.iterator().next();
service.sayHello();
编译并运行:
#> ./run-simple-module-app.sh
Hello, Modules!
Hello!
我们使用这些指令更明确地说明如何使用我们的代码。 我们可以将实现放入私有包中,同时将接口暴露在公共包中。 这使我们的代码更加安全,并且几乎没有额外的开销。 继续尝试其他一些指令,以了解有关模块及其工作方式的更多信息。
8. 将模块添加到未命名模块
**未命名的模块概念类似于默认包。**因此,它不被认为是真正的模块,但可以被视为默认模块。 如果一个类不是命名模块的成员,那么它将自动被视为该未命名模块的一部分。 有时,为了确保模块图中的特定平台、库或服务提供者模块,我们需要将模块添加到默认根集中。例如,当我们尝试使用 Java 9 编译器按原样运行 Java 8 程序时,我们可能需要添加模块。
通常,将命名模块添加到默认根模块集中的选项是*–add-modules module* (,module)*其中module是模块名称。 例如,要提供对所有java.xml.bind模块的访问,语法应该是:
--add-modules java.xml.bind
要在 Maven 中使用它,我们可以将其嵌入到maven-compiler-plugin 中:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>9</source>
<target>9</target>
<compilerArgs>
<arg>--add-modules</arg>
<arg>java.xml.bind</arg>
</compilerArgs>
</configuration>
</plugin>