类路径上的代码 - 未命名模块
模块系统希望所有内容都是模块,以便它可以统一地应用其规则,但同时,创建模块不是强制性的(这样做将不向后兼容)。调和这两个看似矛盾的要求的机制是未命名模块。它包含来自类路径的所有类,并对其应用了一些特殊规则,但一旦完成,它就像任何其他模块一样工作。
这意味着如果您从类路径启动代码,未命名模块将起作用。除非您的应用程序非常小,否则它可能需要增量模块化,这涉及混合 JAR 和模块、类路径和模块路径。这使得了解模块系统的“类路径模式”如何工作变得很重要。
注意:您需要了解 模块系统基础知识 才能充分利用本文。
未命名模块
未命名模块包含所有“非模块化类”,它们是
- 在编译时,如果类不包含模块描述符,则正在编译的类
- 在编译和运行时,从类路径加载的所有类
所有模块都有三个核心属性,这对未命名模块也是如此
- 名称:未命名模块没有名称(有道理,对吧?),这意味着没有其他模块可以在其声明中提及它(例如,要求它)
- 依赖项:未命名模块读取所有其他进入图的模块
- 导出:未命名模块导出其所有包,并 为反射打开它们
与未命名模块形成对比的是,所有其他模块都被称为命名模块。 服务 在 META-INF/services
中提供,可供 ServiceLoader
使用。
虽然并不完全直观,但未命名模块的概念是有道理的。这里您有井然有序的模块图,在那里,在旁边,您有类路径的混乱,被归入它自己的自由放任模块,并具有一些特殊属性。
类路径的混乱
未命名模块的主要目标是捕获类路径内容并使其在模块系统中工作。由于类路径上 JAR 之间从未有任何边界,因此现在建立它们毫无意义,因此整个类路径只有一个未命名模块。在其中,就像在类路径上一样,所有公共类都可以相互访问,并且包可以跨 JAR 拆分。
未命名模块的独特作用及其对向后兼容性的关注赋予它一些特殊属性。一个是 Java 9 到 16 中对强封装 API 的间歇性访问。另一个是它没有暴露给应用于命名模块的许多检查。因此,在它和其他模块之间拆分的包不会被发现,并且类路径部分根本不可用。(这意味着您可能会收到缺少类的错误,而这些类实际上存在于类路径上,如果同一个包也存在于命名模块中。)
一个有点违反直觉且容易出错的细节是,究竟是什么构成了未命名模块。似乎很明显,模块化 JAR 成为模块,因此普通 JAR 进入未命名模块,对吧?但事实并非如此,未命名模块负责类路径上的所有 JAR,无论是否模块化。因此,模块化 JAR 不一定会被加载为模块!因此,如果一个库开始提供模块化 JAR,其用户绝不会被迫将它们用作模块。相反,他们可以将它们留在类路径上,在那里它们的代码被捆绑到未命名模块中。这允许生态系统几乎独立地进行模块化。
要尝试一下,您可以将以下两行代码放入一个类中,并将该类打包为模块化 JAR
String moduleName = this.getClass().getModule().getName();
System.out.println("Module name: " + moduleName);
从类路径启动时,输出为 Module name: null
,表明该类最终位于未命名模块中。从模块路径启动时,您将获得预期的 Module name: $MODULE
,其中 $MODULE
是您为模块指定的名称。
未命名模块的模块解析
未命名模块与模块图其余部分关系的一个重要方面是它可以读取哪些其他模块。如前所述,这些都是进入图的模块。但哪些模块是那些?请记住,从 模块系统基础知识 中,模块解析通过从根模块(特别是初始模块)开始构建模块图,然后迭代地添加所有它们的直接和传递依赖项。如果正在编译的代码或应用程序的 main
方法位于未命名模块中,就像从类路径启动应用程序时一样,这将如何工作?毕竟,普通 JAR 不会表达任何依赖项。
如果初始模块是未命名的,模块解析将从一组预定义的根模块开始。根据经验,这些是在运行时找到的模块,但实际规则更详细一些
- 成为根的java.* 模块的精确集合取决于java.se 模块的存在(即表示整个 Java SE API 的模块;它存在于完整的 JRE 映像中,但可能不存在于使用
jlink
创建的自定义运行时映像中)- 如果java.se 可观察,它将成为根。
- 如果它不存在,每个java.* 模块至少导出一个包 不带限定符 将成为根。
- 除了java.* 模块之外,运行时中每个其他模块(不是孵化模块)并且至少导出一个不带限定符的包将成为根模块。这与jdk.* 模块特别相关。
- 使用
--add-modules
列出的模块 始终是根模块。
请注意,以未命名模块作为初始模块,根模块集始终是运行时映像中包含的模块的子集。模块路径上的模块永远不会被解析,除非使用 --add-modules
显式添加。如果您手工制作了模块路径以包含您需要的精确模块,您可能希望使用 --add-modules ALL-MODULE-PATH
添加所有模块,如 本文 中所述。
取决于未命名模块
模块系统的主要目标之一是可靠的配置:模块必须表达其依赖项,并且模块系统必须能够保证它们的存在。我们讨论了具有模块描述符的显式模块,但如果我们尝试将可靠配置扩展到类路径会发生什么?
一个思想实验
想象一下,模块可以依赖于类路径内容,也许在它们的描述符中使用类似 requires class-path
的东西。模块系统可以为这种依赖关系提供哪些保证?事实证明,几乎没有。只要类路径上至少存在一个类,模块系统就必须假设依赖关系已满足。这将不是很有帮助。更糟糕的是,它会严重破坏可靠配置,因为您最终可能会依赖于一个 requires class-path
的模块。但这几乎不包含任何信息 - 究竟需要在类路径上放置什么?
进一步推测这个假设,想象一下两个模块com.example.framework 和com.example.library 依赖于同一个第三个模块,比如 SLF4J。一个在 SLF4J 模块化之前声明了依赖关系,因此 requires class-path
,另一个声明了对模块化 SLF4J 的依赖关系,因此 requires org.slf4j
。现在,依赖于com.example.framework 和com.example.library 的任何人都将 SLF4J JAR 放置在哪个路径上?无论他们选择哪个,模块系统都必须确定两个传递依赖项之一没有满足。
仔细思考一下,我们可以得出结论,如果您想要可靠的模块,依赖于任意类路径内容不是一个好主意。正是出于这个原因,没有 requires class-path
。
因此,未命名
那么如何最好地表达最终持有类路径内容的模块不能依赖于它?在一个使用名称引用其他模块的模块系统中?不给那个模块起名字,让它成为未命名的,这么说起来似乎很合理。就是这样:未命名模块没有名称,因为任何模块都不应该在 requires
指令中引用它 - 或者任何其他指令,就此而言。没有 requires
,就没有可读性优势,没有这种优势,未命名模块中的代码就无法访问模块。
总之,对于一个显式模块要依赖于一个工件,该工件必须位于模块路径上。这很可能意味着您将普通 JAR 放置在模块路径上,这会将它们变成自动模块 - 我们将在下一节中探讨这个概念。
上次更新: 2021 年 9 月 14 日