Java9模块化:你了解过吗?(入门使用指南)

发表时间: 2023-06-25 16:55

1.概述

Java9在包之上引入了一个新的抽象级别,正式名称为 Java 平台模块系统(JavaPlatform Module System,JPMS) ,简称模块

接下来,这篇文章将会介绍模块化的基本概念,同时我还将构建一个简单的项目来演示我们将在本指南中学习的所有概念

2.什么是模块

首先,为了更好的去使用模块之前,我们需要去了解什么是模块。

模块是一组紧密相关的包和资源同时伴随一个模块描述符文件。

换句话说,可以简单抽象为它是Java包上的包,以便允许我们的代码能够更加的被复用。

2.1.模块中的包

模块中的包与我们自 Java 诞生以来一直在使用的 Java 包完全相同。

当我们创建一个模块时,我们在内部组织包中的代码,就像我们以前对任何其他项目所做的那样。

除了组织我们的代码之外,还使用包来确定哪些代码可以在模块之外公开访问。我们将在本文后面花更多时间讨论这个问题。

2.2.模块中的资源

每个模块都需要负责和管理其资源文件,比如:媒体资源或者配置文件。

以前,我们将所有资源放在项目的根级别,并手动管理哪些资源属于应用程序的不同部分。

使用模块,我们可以将所需的图像和 XML 文件与所需的模块一起发布,从而使我们的项目更容易管理。

2.3.模块描述符

当我们创建一个模块时,我们包含了一个描述符文件,它从以下几个方面定义了一个新模块:

  • 名称(Name) 模块名称
  • 依赖(Dependencies) 该模块所依赖的其他模块的列表
  • 公有包(public Packages)我们希望从模块外部访问的所有包的列表
  • 服务提供(Services Offered)我们可以提供其它模块可以使用的服务实现
  • 服务消费(Services Consumed)允许当前模块成为服务的使用者
  • 反射权限(Reflection Permissions)显式地允许其他类使用反射来访问包的私有成员

模块命名规则类似于我们如何命名包(允许用点,但是不允许用破折号)。一般常用的就是项目式风格(my.module)或者域名反转(com.demo.mymodule)风格。

我们需要列出所有希望公开的包,因为默认情况下所有包都是模块私有的。

反射也是如此。默认情况下,我们不能对从另一个模块导入的类使用反射。

在本文的后面,我们将看到如何使用模块描述符文件的示例。

2.4.模块类型

在新的模块系统中有四种类型的模块:

  • 系统模块(System Modules)这些可以通过 list-module 命令列出所有的系统模块,它们包含Java SE和JDK模块。
  • 应用模块(Application Modules)当我们决定使用模块时,通常需要构建这些模块。它们在组装的 JAR 中,并且命名和定义在已编译的module-info.class
  • 自动模块(Automatic Modules我们可以通过将现有的 JAR 文件添加到模块路径来包含非官方模块。模块的名称将从 JAR 的名称派生(使用JAR名称)。自动模块将对路径加载的每个其他模块具有完全读访问权限。
  • 未命名模块(Unnamed Module)当一个类或 JAR 被加载到类路径(而不是模块路径)时,它会自动添加到未命名的模块中。这是一个全面覆盖的模块,可以用以前编写的 Java 代码来维护向下兼容。

2.5.模块发布

模块可以通过以下两种方式发布:作为一个 JAR 文件或作为一个编译的项目。

我们可以创建由一个“主应用程序”和几个库模块组成的多模块项目。

但是我们必须小心,因为每个 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 {//可选的模块指令}

我们使用 module 关键字开始模块声明,然后使用模块名称跟随声明。该模块将使用此声明,但是我们通常需要更多信息。这就是模块指令的用武之地。

4.1. 模块指令Requires

我们的第一个指令是requires这个模块指令允许我们声明模块依赖项:

module my.module {    requires module.name;}

现在,my.module 对 module.name 既有运行时依赖关系,也有编译时依赖关系。

当我们使用此指令时,我们的模块可以访问从依赖项导出的所有公共类型。

4.2.Requires Static

有时我们编写的代码引用另一个模块,但是我们库的用户永远不会想要使用这些代码。

例如,我们可能会写一个格式化打印内部状态的工具,同时此刻已经包含一个存在的日志模块。但是,不是所有使用我们库的用户都想要这个工具,并且他们也不想包含额外的日志库。

在这些情况下,我们希望使用一个可选的依赖项。通过使用requires static指令,我们创建了一个仅编译时的依赖项:

module my.module {    requires static module.name;}

4.3.Requires Transitive

通常为了方便处理这些模块库,我们需要这些库具有传递性。

但是,我们需要确保引入我们代码的任何模块也将引入这些额外的“传递”依赖项,否则它们将无法工作。

幸运的是,我们可以使用requires transitive指令来强制任何下游消费者也能使用我们所需的依赖项:

module my.module {    requires transitive module.name;}

现在,当一个开发人员需要 my.module 时,他们不必说还需要 module.name 才能让我们的模块继续工作。

4.4.Exports(导出)

默认情况下,模块不向其他模块公开任何 API。这种强封装是最初创建模块系统的关键原因之一。

我们的代码明显更加安全,但是现在我们需要显式地向外界开放我们的 API,如果我们希望它可用的话。

我们使用 export 指令暴露所有的公共成员:

module my.module {    exports com.my.package.name;}

现在,当有人需要 my.module 时,他们可以访问我们的 com.my.package.name 包中的 public 类型,但不能访问其他任何包。

4.5.Exports ... To

我们可以使用exports...to来开放我们的公共类给所有访问模块。

但是,如果我不想让所有模块都能访问我们的api怎么办?

我们可以使用 export... to 指令限制哪些模块可以访问我们的 API。

类似exports指令,我们声明了一个可以导出的包。但是,我们同时列出了哪些模块是可以通过requires指令来导入这些包的。

module my.module {    export com.my.package.name to com.specific.package;}

4.6.Uses

一个服务是其他类可以使用的特定接口或抽象类的实现。

我们使用 uses 指令指定模块使用的服务。

注意,我们使用的类名是服务的接口或抽象类,而不是实现类:

module my.module {    uses class.name;}

在这里我们应该注意到,在需求指令和使用指令之间有一个区别。

我们可能需要一个模块,该模块提供我们想要使用的服务,但该服务实现了来自其传递依赖关系之一的接口。

为了以防万一,我们不强制模块需要所有的传递依赖项,而是使用 uses 指令将所需的接口添加到模块路径中。

4.7.Provides ... With

模块也可以是其他模块可以使用的服务提供者。

指令的第一部分是 provides关键字,这里便是我们放置接口或抽象类名的地方。

接下来,我们有 with 指令,这里提供实现接口或扩展抽象类的实现类名。

module my.module {    provides MyInterface with MyInterfaceImpl;}

4.8.Open

我们前面提到过封装是设计这个模块系统的驱动因素。

在Java9之前,可以使用反射来检查包中的每个类型和成员,甚至是私有类型。没有什么是真正封装的,这个会给库的开发人员带来各种各样的问题。

由于Java9的强制实行了强封装,我们现在必须显式地授予其他模块对我们的类进行反射的权限。

如果我们想继续像旧版 Java 那样允许完全反射,我们可以简单地打开整个模块:

module my.module {  opens com.my.package;}

4.9.Opens

如果我们需要允许私有类型的反射,但是我们不想暴露所有的代码,我们可以使用Opens指令去暴露指定的包。

但是请记住,这个将会开放这个包给整个外界,所以请确保这是你想要做的:

module my.module {  opens com.my.package;}

4.10.Opens ... To

有时候反射确实能给我们带来很多便利性,但是我们仍然需要从封装中获得尽可能多的安全性。我们可以有选择地打开我们的包到一个预先批准的模块列表,在这种情况下,使用 open... to 指令:

module my.module {    opens com.my.package to moduleOne, moduleTwo;}

5.命令行参数

到目前为止,对 Java9模块的支持已经添加到 Maven 和 Gradle 中,因此您不需要对项目进行大量的手工构建。但是,了解如何从命令行使用模块系统仍然很有价值。

我们将在下面的完整示例中使用命令行来帮助巩固整个系统在我们头脑中的工作方式。

  • module-path – 我们使用-module-path选项来指定模块路径,这是包含你的模块的一个或多个目录的列表。
  • add-reads – 我们可以通过使用命令行-add-reads来等效代替描述文件中的requires指令。
  • add-exports – 代替exports指令
  • add-opens – 代替open指令
  • add-modules 将模块列表添加到默认的模块集中
  • list-modules打印所有模块及其版本字符串的列表
  • patch-module 在模块中添加或重写类
  • illegal-access=permit|warn|deny 要么通过显示单个全局警告来放松强封装,要么显示每个警告,要么错误导致失败。默认情况下是允许的。

6.可见性

我们应该花一点时间来讨论代码的可见性。

许多库和框架都是通过反射来体现出其威力(比如:Junit和Spring)。

在 Java9中默认情况下,我们只能访问导出包中的公共类、方法和字段。即使我们使用反射去获取访问非公开的成员并设置setAccessible(true),我们也不能访问这些成员。

我们可以使用open,opens,和opens...to来为反射授予仅运行时访问,注意的是,仅运行时。

我们无法针对私有类型进行编译,而且无论如何也不需要这样做。

如果我们必须访问一个模块进行反射,而且我们不是该模块的所有者(例如,我们不能使用 open... to 指令) ,那么可以使用命令行-add-open 选项允许自己的模块在运行时对锁定的模块进行反射访问。

这里唯一需要注意的是,您需要访问命令行参数,这些参数用于运行模块以使其工作。

7.最佳实践

现在我们知道了模块是什么以及如何使用它们,让我们继续构建一个简单的项目来演示我们刚刚学到的所有概念。

为了简单起见,我们不会使用 Maven 或 Gradle。相反,我们将依靠命令行工具来构建我们的模块。

7.1.建立项目

首先,我们需要设置我们的项目结构。我们将创建几个目录来组织我们的文件。

首先创建项目文件夹:

mkdir module-projectcd module-project

这是我们整个项目的基础,所以在这里添加文件,如 Maven 或 Gradle 构建文件,其他源目录和资源。

我们还放置了一个目录来保存所有特定于项目的模块。

接下来,我们创建一个模块目录:

mkdir simple-modules

下面是我们的项目结构:

module-project|- simple-modules  |- hello.modules    |- com      |- demo        |- modules          |- hello  |- main.app    |- com      |- demo        |- modules          |- main

7.2.第一个模块

现在我们已经有了基本的结构,让我们添加第一个模块。

在 simple-module 目录下,创建一个名为 hello.module 的新目录。

除了遵循包命名规则之外,我们可以给这个命名任何我们想要的东西。如果愿意,我们甚至可以使用主包的名称作为模块名称,但是通常,我们希望使用与创建此模块的 JAR 相同的名称。

在我们的新模块下,我们可以创建我们想要的包。在我们的示例中,我们将创建一个包结构:

com.demo.modules.hello

接下来,在这个包中创建一个名为 HelloModules.java 的新类:

package com.demo.modules.hello;public class HelloModules {    public static void doSomething() {        System.out.println("Hello, Modules!");    }}

最后,在 hello.module 根目录中,添加我们的模块描述符; module-info. java:

module hello.modules {    exports com.demo.modules.hello;}

为了使这个示例保持简单,我们所做的就是导出 com.demo.modules.hello 包的所有公共成员。

7.3.第二个模块

我们的第一个模块已经好了,但它什么都不做。

我们现在可以创建一个使用它的第二个模块。

在 simple-module 目录下,创建另一个名为 main.app 的模块目录。这次我们将从模块描述符开始:

module main.app {    requires hello.modules;}

我们不需要向外界透露任何信息。相反,我们所需要做的就是依赖于我们的第一个模块,因此我们可以访问它导出的公共类。

现在我们可以创建一个使用它的应用程序。

创建一个新的包结构:com.demo.modules.main。

接下来,创建一个新的类文件叫:MainApp.java。

package com.demo.modules.main;import com.demo.modules.hello.HelloModules;public class MainApp {    public static void main(String[] args) {        HelloModules.doSomething();    }}

这就是我们演示模块所需的全部代码。我们的下一步是从命令行构建并运行此代码。

7.4.构建我们的模块

要构建我们的项目,我们可以创建一个简单的 bash 脚本并将其放在项目的根目录下。

创建一个名为 compile-simple-modules.sh 的文件:

#!/usr/bin/env bashjavac -d outDir --module-source-path simple-modules $(find simple-modules -name "*.java")

这个命令有两部分,javac 和 find 命令。

Find 命令只是在 simple-module 目录下输出所有. java 文件的列表。然后,我们可以将该列表直接提供给 Java 编译器。

与旧版 Java 不同的是,我们必须提供一个 module-source-path 参数来告诉编译器它正在构建模块。

运行此命令后,我们将拥有一个 outDir 文件夹,其中包含两个已编译的模块。

7.5.运行我们的代码

现在我们终于可以运行代码来验证模块是否正常工作了。

在项目的根目录中创建另一个文件: run-simple-module-app.sh。

#!/usr/bin/env bashjava --module-path outDir -m main.app/com.demo.modules.main.MainApp

要运行一个模块,我们必须至少提供模块路径和主类。如果一切正常,你应该看到:

>$ ./run-simple-module-app.sh Hello, Modules!

总结

在这篇文章中,我们重点介绍了新的 Java9模块系统的基础知识。我们开始讨论什么是模块。接下来,我们讨论了如何发现 JDK 中包含哪些模块。我们还详细介绍了模块声明文件。最后,我们根据前面的基础概念构建和运行了我们自己的简单的模块应用。到现在你应该对Java9的模块化有一定的了解了吧。