Java9模块化指南

1. 概述

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

在本文中,我们将介绍新系统并讨论其各个方面。

2. 什么是模块?

首先,我们需要先了解模块是什么,然后才能了解如何使用它们。

模块是一组密切相关的包和资源以及一个新的模块描述符文件。

换句话说,它是一个“Java包的包”的抽象,允许我们使代码更加 可重用

2.1 Packages

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

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

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

2.2 Resources

每个模块负责其资源,如媒体或配置文件。

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

通过模块,我们可以将所需的图像和XML文件与需要的模块一起发送,从而使我们的项目更 易于管理

2.3 模块描述符

创建模块时,我们会包含一个描述符文件,该文件定义了新模块的几个方面:

  • Name–我们模块的名称
  • 依赖项–此模块依赖的其他模块的列表
  • 公共包–我们希望从模块外部访问的所有包的列表
  • 提供的服务–我们可以提供其他模块可以使用的服务实现
  • 已使用的服务–允许当前模块成为服务的使用者
  • 反射权限–显式允许其他类使用反射来访问包的私有成员

模块命名规则类似于我们命名包的方式(允许点,不允许破折号)。两种项目风格都很常见(我的模块)或反向DNS(com.baeldung.mymodule)样式名称。我们将在本指南中使用项目样式。

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

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

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

2.4 模块类型

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

  • 系统模块–这些是我们运行上面的list Modules命令时列出的模块。它们包括javase和JDK模块。
  • 应用程序模块–这些模块是我们决定使用模块时通常要构建的。它们在编译模块中命名和定义-信息类包含在组合JAR中的文件。
  • 自动模块–我们可以通过将现有JAR文件添加到模块路径来包含非官方模块。模块的名称将从JAR的名称派生。自动模块将对路径加载的每个其他模块具有完全读取权限。
  • 未命名模块–当类或JAR加载到类路径而不是模块路径时,它会自动添加到未命名模块。它是一个catch-all模块,用于保持与以前编写的Java代码的向后兼容性。

2.5 分配

模块可以以两种方式之一分发:作为JAR文件或作为“分解”编译项目。当然,这与任何其他Java项目都是一样的,所以这并不奇怪。

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

但是我们必须小心, 因为每个JAR文件只能有一个模块

在设置构建文件时,我们需要确保将项目中的每个模块打包为一个单独的jar。

3. 默认模块

当我们安装Java9时,我们可以看到JDK现在有了一个新的结构。

他们把所有的原始软件包都搬进了新的模块系统。

我们可以在命令行中键入以下模块:

java --list-modules

这些模块分为四大类:java、javafx、jdk和Oracle。

  • java模块是核心SE语言规范的实现类。
  • javafx模块是fxui库。
  • JDK模块本身需要什么就保存什么。
  • 最后,任何特定于Oracle的内容都在Oracle模块中。

4. 模块声明

要设置模块,我们需要在名为module的包的根目录下放置一个特殊 module-info.java .

这个文件称为模块描述符,包含构建和使用新模块所需的所有数据。

我们用一个声明构造模块,声明的主体为空或由模块指令组成:

module myModuleName {
    // all directives are optional
}

我们用 module 关键字开始module声明,然后是module的名称。

模块将处理此声明,但我们通常需要更多信息。

这就是模块指令的作用。

4.1 要求

我们的第一个指令是要求。此模块指令允许我们声明模块依赖项:

module my.module {
    requires module.name;
}

现在,我的模块对具有 运行时和编译时 依赖关系模块名称。

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

4.2 需要静态

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

例如,我们可以编写一个实用函数,当存在另一个日志模块时,它可以很好地打印出我们的内部状态。但是,并不是我们库的每个消费者都会想要这个功能,他们也不想包括一个额外的日志库。

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

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

4.3 需要传递的

我们通常与lib库合作,使我们的生活更轻松。

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

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

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

现在,当开发人员需要我的模块,他们也不用说模块名称让我们的模块继续工作。

4.4 出口

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

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

我们使用 exports 指令公开命名包的所有公共成员:

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

现在,当有人需要我的模块,他们将可以访问我们的 com.my.package.name ,但不是任何其他包。

4.5 Exports … To

我们可以利用Exports … To世界开放我们的公共库。

但是,如果我们不想让整个世界都访问我们的API呢?

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

与出口指令类似,我们将一个包声明为已出口。但是,我们还列出了允许导入此包的模块。让我们看看这是什么样子:

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

4.6 使用

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

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

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

module my.module {
    uses class.name;
}

我们应该注意到 requires 指令和 uses 指令之间有区别。

我们可能需要一个模块来提供我们想要使用的服务,但是该服务从它的一个可传递依赖项实现一个接口。

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

4.7 Provides … With

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

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

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

下面是它的外观:

module my.module {
    provides MyInterface with MyInterfaceImpl;
}

4.8 Open

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

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

因为Java9强制了强封装,所以我们现在必须显式地授予其他模块反映类的权限。

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

open module my.module {
}

4.9 Opens

如果我们需要允许私有类型的反射,但不希望所有代码都公开,那么可以使用 opens 指令公开特定的包。

但请记住,这 将向全世界开放软件包,因此请确保这是您想要的

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

4.10 Opens … To

好吧,反射有时是很好的,但是我们仍然希望从封装中获得尽可能多的安全性。我们可以使用 opens…to 指令有选择地将包打开到预先批准的模块列表中,在这种情况下:

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

5. 命令行选项

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

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

  • module-path–我们使用–module path选项指定模块路径。这是包含模块的一个或多个目录的列表。
  • add reads–不依赖模块声明文件,我们可以使用与requires指令等效的命令行;–add reads。
  • add exports–exports指令的命令行替换。
  • add opens–替换模块声明文件中的open子句。
  • add modules–将模块列表添加到默认模块集中
  • 列出模块–打印所有模块及其版本字符串的列表
  • 补丁模块–在模块中添加或重写类
  • 非法访问=允许|警告|拒绝–通过显示单个全局警告来放松强封装,显示每个警告,或者失败并出现错误。默认值为permit。

6. Visibility

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

许多库依赖于反射来发挥它们的魔力(想到JUnit和Spring)。

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

我们可以使用 openopensopens…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
      |- baeldung
        |- modules
          |- hello
  |- main.app
    |- com
      |- baeldung
        |- modules
          |- main

7.2 我们的第一个模块

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

simple modules 目录下,创建一个名为 hello .

我们可以给它命名任何我们想命名的东西,但要遵循包命名规则(例如,句点分隔单词等)。如果需要的话,我们甚至可以使用主包的名称作为模块名,但是通常,我们希望使用相同的名称来创建这个模块的JAR。

在新模块下,我们可以创建所需的包。在本例中,我们将创建一个包结构:

com.baeldung.modules.hello

接下来,创建一个名为 HelloModules.java 在这个包裹里。我们将保持代码简单:

package com.baeldung.modules.hello;

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

最后,在你好。模块根目录,添加模块描述符;module-info.java:

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

为了简单起见,我们所做的就是导出 com.baeldung.modules.hello 包。

7.3 我们的第二个模块

我们的第一个模块很好,但它什么也做不了。

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

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

module main.app {
    requires hello.modules;
}

我们不需要把任何东西暴露在外面。相反,我们需要做的只是依赖于我们的第一个模块,这样我们就可以访问它导出的公共类。

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

创建新的包结构: com.baeldung.modules.main

现在,创建一个名为主应用程序 MainApp.java .

package com.baeldung.modules.main;

import com.baeldung.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")

这个命令有两个部分, javacfind 命令。

find 命令只是输出simple modules目录下所有.java文件的列表。然后我们可以将该列表直接输入Java编译器。

我们要做的唯一不同于旧版本的Java的事情是提供一个模块源路径参数来通知编译器它正在构建模块。

一旦我们运行这个命令,我们就会有一个outDir文件夹,里面有两个编译过的模块。

7.5 运行我们的代码

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

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

#!/usr/bin/env bash
java --module-path outDir -m main.app/com.baeldung.modules.main.MainApp

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

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

7.6 添加服务

现在我们已经基本了解了如何构建模块,让我们把它变得更复杂一点。

我们将了解如何使用 provides…withuses 指令。

首先在中定义一个新文件你好。模块名为的模块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.baeldung.modules.hello.HelloInterface with com.baeldung.modules.hello.HelloModules;

如我们所见,我们声明了接口以及实现它的类。

接下来,我们需要使用这个服务。在我们的main.app模块,让我们在模块中添加以下module-info.java:

uses com.baeldung.modules.hello.HelloInterface;

最后,在我们的主要方法中,我们可以通过ServiceLoader使用此服务:

Iterable services = ServiceLoader.load(HelloInterface.class);
HelloInterface service = services.iterator().next();
service.sayHello();

编译并运行:

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

我们使用这些指令来更明确地说明如何使用我们的代码。

我们可以将实现放在私有包中,同时将接口公开在公共包中。

这使得我们的代码更加安全,只需要很少的额外开销。

继续并尝试一些其他指令,以了解有关模块及其工作方式的更多信息。

8. 向未命名模块添加模块

与未命名模块的概念类似。因此,它不被认为是一个真正的模块,但可以被视为 默认模块

如果类不是命名模块的成员,则它将自动被视为此未命名模块的一部分。

有时,为了确保模块图中的特定平台、库或服务提供者模块,我们需要将模块添加到默认根集中。例如,当我们试图用Java9编译器运行Java8程序时,我们可能需要添加模块。

通常, 将命名模块添加到默认根模块集中的选项–add modules(,) *其中 是模块名称。

例如,提供访问所有 java.xml.bind 语法为:

--add-modules java.xml.bind

要在Maven中使用它,我们可以将其嵌入Maven编译器插件:

    org.apache.maven.plugins
    maven-compiler-plugin
    3.8.0
    
        9
        9
        
            --add-modules
            java.xml.bind
        
    

9. 结论

在这个广泛的指南中,我们重点介绍了新的Java9模块系统的基础知识。

我们从讨论什么是模块开始。

接下来,我们讨论了如何发现JDK中包含哪些模块。

我们还详细介绍了模块声明文件。

我们通过讨论构建模块所需的各种命令行参数来完善这个理论。

最后,我们将前面的知识应用到实践中,并在模块系统的基础上创建了一个简单的应用程序。

要查看此代码及更多内容,请务必在Github上查看它。