当前教程
增量模块化与自动模块
系列中的下一篇

系列中的上一篇: 类路径上的代码 - 未命名模块

系列中的下一篇: 在命令行上构建模块

增量模块化与自动模块

模块系统要求模块的所有依赖项都必须在模块路径(或运行时)上找到。如果只有模块化 JAR 文件可以在模块路径上工作,则项目的所有依赖项都必须在项目本身成为模块之前成为模块,而大型项目则必须一步到位地进行模块化。为了避免这种生态系统范围内的自下而上的模块化工作以及大型项目的“大爆炸”式模块化,模块系统还允许在模块路径上使用普通 JAR 文件,这些文件会变成自动模块。一旦应用了一些特殊规则,它们就可以像所有其他模块一样工作。其中一个特殊规则是,自动模块可以读取未命名模块,这使得它们可以充当从模块路径到类路径的桥梁。

注意: 您需要了解 模块系统基础知识、关于 未命名模块 以及 隐式可读性,才能充分利用本文。

自动模块

对于模块路径上每个没有模块描述符的 JAR 文件,模块系统都会创建一个自动模块。与任何其他模块一样,它具有三个核心属性

  • 名称: 自动模块的名称可以在 JAR 的清单文件中使用 Automatic-Module-Name 标头定义(下面会详细介绍);如果缺少,模块系统会从文件名生成一个名称
  • 依赖项: 自动模块读取所有使其进入图表的其他模块,包括未命名模块
  • 导出: 自动模块导出其所有包,并 为反射打开它们

服务META-INF/services 中提供,可供 ServiceLoader 使用。

自动模块是完整的命名模块,这意味着

  • 它们可以在其他模块的声明中通过其名称引用,例如,要求它们。
  • 即使在 Java 9 到 16 上,它们也不受 JDK 模块强封装的例外 的约束。
  • 它们会受到可靠性检查的约束,例如拆分包。

要尝试使用自动模块,您可以将以下两行代码放入一个类中,并将该类打包为一个普通 JAR 文件

String moduleName = this.getClass().getModule().getName();
System.out.println("Module name: " + moduleName);

从类路径启动时,输出为 Module name: null,表明该类最终位于未命名模块中。从模块路径启动时,您将获得预期的 Module name: $JAR,其中 $JAR 是您为 JAR 文件指定的名称。如果您在清单文件中添加一个 Automatic-Module-Name 标头,该标头定义了一个名称,那么从模块路径启动 JAR 文件时,您将看到该名称。要尝试依赖自动模块,您可以创建一个第二个项目,并在其模块声明中添加 requires $JAR

自动模块名称 - 小细节,大影响

将普通 JAR 文件转换为模块的主要目的是能够在模块声明中要求它们。为此,它们需要一个名称,但由于缺少模块描述符,它将从何而来呢?

首先是清单条目,然后是文件名

确定普通 JAR 文件的模块名称的一种方法依赖于其清单文件,该文件是 JAR 文件 META-INF 文件夹中的 MANIFEST.MF 文件。如果模块路径上的 JAR 文件不包含描述符,则模块系统会遵循一个两步过程来确定自动模块的名称

  1. 它会在清单文件中查找 Automatic-Module-Name 标头。如果找到,它会使用相应的 value 作为模块的名称。
  2. 如果清单文件中不存在该标头,则模块系统会从文件名推断出模块名称。

能够从清单文件中推断出模块名称要好得多,因为它更加稳定 - 这一点将在下面详细介绍。从文件名推断出模块名称的确切规则有点复杂,但细节并不重要 - 以下是要点

  • JAR 文件名通常以版本字符串结尾(例如 -2.0.5)。这些字符串会被识别并忽略。
  • 除字母和数字之外的每个字符都会变成一个点。

此过程可能会导致不幸的结果,即生成的模块名称无效。一个例子是字节码操作工具 ByteBuddy: 它在 Maven Central 中发布为 byte-buddy-$VERSION.jar,这导致自动模块名称为 byte.buddy(在它定义了正确的名称之前)。不幸的是,这是非法的,因为 byte 当然是一个 Java 关键字。

找出名称

如果您需要找出普通 JAR 文件的自动模块名称,您可以对 JAR 文件运行 jar --describe-module --file $FILE。不幸的是,这并不能告诉您名称是从清单条目中选取的还是从文件名中选取的。要找出这一点,您有几个选择

  • 使用 jar --file $JAR --extract META-INF/MANIFEST.MF 提取清单文件,并手动查看它。
  • 在 Linux 上,unzip -p $JAR META-INF/MANIFEST.MF 会将清单文件打印到终端,从而节省您打开文件的时间。
  • 重命名文件并再次运行 jar --describe-module

何时设置 Automatic-Module-Name

如果您维护的是一个公开发布的项目,这意味着其工件可通过 Maven Central 或其他公共存储库获得,那么您应该仔细考虑何时在清单文件中设置 Automatic-Module-Name。正如前面提到的,它使将您的项目用作自动模块更加可靠,但也承诺将来显式模块将成为当前 JAR 文件的直接替换。您实际上是在说:“这就是模块的样子,我只是还没有发布它们”。

定义自动模块名称会邀请您的用户开始依赖您的项目工件作为模块,这一事实有一些重要的影响

  • 未来模块的名称必须与您现在声明的名称完全相同。(否则,可靠的配置会让您的用户感到困扰,因为模块丢失了。)
  • 工件结构必须保持不变,因此您不能开始将支持的类或包从一个 JAR 文件移动到另一个 JAR 文件。(即使没有模块,也不建议这样做,但使用类路径时,哪个 JAR 文件包含一个类并不重要,因此您可以做到这一点。另一方面,在模块系统中,类的来源非常重要,因为可访问性迫使用户要求正确的模块。)
  • 该项目在 Java 9 及更高版本上运行良好。如果它需要命令行选项或其他解决方法,则这些方法有详细的文档。(否则,您无法确定您的代码中是否隐藏着一些问题,这些问题会使其他承诺变得毫无意义。)

自动模块的模块解析

自动模块是从普通 JAR 文件创建的,因此它们没有显式依赖项,这就引出了一个问题,即它们在解析过程中如何表现。JAR 文件往往相互依赖,如果模块系统只解析显式要求的自动模块,则所有其他自动模块都需要 使用 --add-modules 添加到图表中。想象一下,对于一个大型项目,您决定将数百个依赖项都放在模块路径上,然后这样做。

为了防止这种过度的、脆弱的手动模块添加,一旦模块系统遇到第一个显式要求的自动模块,它就会拉入所有自动模块。换句话说,您要么获得所有普通 JAR 文件作为自动模块(如果至少有一个被要求或添加),要么一个都没有(否则)。另一个方面是,自动模块 隐式地对其他自动模块具有可读性,这意味着任何读取一个模块的模块都会读取所有这些模块。

如果自动模块只能读取其他命名模块,那么我们现在就完成了。一旦您将一个普通 JAR 文件放在模块路径上,它的所有直接依赖项都必须也放在模块路径上,然后是它们的依赖项,依此类推,直到所有传递依赖项都被视为模块,无论是显式模块还是自动模块。

但是,将普通 JAR 文件转换为自动模块可能无法正常工作,因为会对其进行检查(例如,搜索拆分包)。因此,能够将普通 JAR 文件留在类路径上,并将它们加载到未命名模块中会很好。事实上,模块系统允许这样做,它让自动模块读取未命名模块,这意味着它们的依赖项可以位于类路径模块路径上。

当我们暂时关注平台模块时,我们会发现自动模块无法表达对它们的依赖关系。因此,模块图表可能包含它们,也可能不包含它们,如果它不包含它们,则自动模块很可能在运行时因缺少类而引发异常。解决此问题的唯一方法是,项目的维护人员公开记录他们需要的模块,以便他们的用户可以确保存在所需的模块。用户可以通过以下两种方式做到这一点:要么显式地要求它们,例如在依赖自动模块的模块中,要么使用 --add-modules

依赖自动模块

自动模块的唯一目的是依赖普通 JAR 文件,因此,即使所有依赖项都还没有模块化,也可以创建显式模块。不过,有一个重要的注意事项:如果 JAR 文件的清单文件中不包含 Automatic-Module-Name 条目,则会从文件名推断出自动模块名称。

但是,根据它们的设置,不同的项目可能会对相同的 JAR 文件使用不同的名称。此外,大多数项目使用 Maven 支持的本地存储库,其中 JAR 文件命名为 ${artifactID}-$VERSION,从该名称中,模块系统很可能会推断出 ${artifactID} 作为自动模块的名称。这很成问题,因为工件 ID 通常不遵循反向域名约定,这意味着一旦项目被模块化,模块名称很可能会发生变化。

总而言之,同一个 JAR 文件在不同的项目中(取决于它们的设置)以及在不同的时间(模块化之前和之后)可能会获得不同的模块名称。这有可能在 downstream 造成混乱,必须不惜一切代价避免!

看起来好像关键错误是通过基于其文件名的模块名称来要求一个普通 JAR 文件。但这通常不是这样 - 在应用程序和其他情况下,开发人员完全控制着要求此类自动模块的模块描述符,使用这种方法是完全可以的。不,错误在于将具有此类依赖关系的模块发布到公共存储库。只有这样,用户才会遇到这种情况,即一个模块隐式地依赖于他们无法控制的细节,这会导致额外的工作,甚至无法解决的差异。

因此,您永远不应该发布(到公开可访问的存储库)需要一个没有 `Automatic-Module-Name` 条目的普通 JAR 文件的模块。只有有了这个条目,自动模块名称才能足够稳定以供依赖。是的,这可能意味着您还不能发布库或框架的模块化版本,必须等待您的依赖项添加该条目。这很不幸,但这样做会对您的用户造成很大的伤害。


最后更新: 2021 年 9 月 14 日


当前教程
增量模块化与自动模块
系列中的下一篇

系列中的上一篇: 类路径上的代码 - 未命名模块

系列中的下一篇: 在命令行上构建模块