龙空技术网

看我如何“定义模块及其属性”,同事高呼太优雅了!

程序员xysam 28

前言:

目前兄弟们对“隐藏模块的编译错误是什么意思”大概比较注意,咱们都需要知道一些“隐藏模块的编译错误是什么意思”的相关资讯。那么小编同时在网摘上网罗了一些对于“隐藏模块的编译错误是什么意思””的相关文章,希望姐妹们能喜欢,大家快快来学习一下吧!

本文内容

1 什么是模块以及模块声明如何定义它们

2 辨识不同类型的模块

3 模块可读性和可访问性

4 理解模块路径

5 用模块解析构建模块图

关于模块,我们已经谈论了很多。模块不仅是模块化应用程序的基石,也是理解模块系统的基石。因此,必须更深入地了解模块是什么,以及它们的属性如何塑造程序的行为。

本文探索了定义、构建和运行模块这3个基本步骤中的第一个,详细解释了什么是模块,以及模块声明如何定义其名称、依赖和API(等下会介绍)。JDK中的一些示例会引你初窥模块世界。本文即将在Java 9中探索模块,并对各种模块进行分类,为你在模块世界中导航。

本文也会讨论模块系统如何(借助扩展、编译器和运行时)与模块进行交互。最后,后面会介绍模块路径,以及模块系统如何解析依赖并基于它们构建模块图如果想进行实践,可以查看ServiceMonitor的master分支,它包含了大多数本文展示的模块声明。

在后文,你将了解如何定义模块的名称、依赖和API,以及模块系统如何基于该信息开展工作。你将能够理解、分析模块系统抛出的错误信息并将其修复。

提示

本文为后文内容奠定了基础,为了让这些关联更明晰,本文包含很多前向引用。如果这些前向引用影响了阅读,请忽略它们;但是,当翻阅本文,寻找某些特定内容时,它们会变得非常重要。

一 模块:模块化应用程序的基石

在对模块进行了这么多讨论后,是时候进行实践了。在学习如何声明模块属性之前,首先看一下两种文件格式——JMOD和模块化JAR。你将在其中接触并进一步了解模块。为了方便本书其余部分的讨论,本文在此对不同类型的模块进行了分类。

1 随JDK发布的Java模块(JMOD)

在Jigsaw项目中,Java代码库被拆分成了大约100个模块,这些模块以一种称为JMOD的新格式交付。它被刻意指明基于JAR格式(本质上是个ZIP文件)以避免使用全新的格式。它只供JDK使用,在此不会进行深入讨论。

我们虽然无法创建JMOD,但仍然可以剖析它。调用java --listmodules,以查阅JRE或JDK所包含的模块。这些信息存储在一个优化过的模块列表文件中——运行时安装的libs目录中的modules文件。

JDK(而非JRE)的jmods目录中也包含裸模块。另外,你可以在与jmods目录相邻的bin目录中找到一个新的工具——jmod,它的describe操作可以用来输出JMOD的属性。

以下代码片段展示了一个剖析JMOD文件的例子。此处,jmod用来描述一个Linux系统中的java.sql模块。JDK 9安装在/opt/jdk-9中。像大多数Java模块一样,java.sql使用了若干个模块系统的高级特性,因此并非所有的细节都会在本文详述。

2 模块化JAR:内生模块

如果不能创建JMOD,那么要如何交付自己创建的模块呢?这就是模块化JAR的作用所在。

定义:模块化JAR和模块描述符

模块化JAR基本上只是普通的JAR,只有一处小细节有所不同。它的根目录包含一个模块描述符:module-info.class文件。(本文将不带模块描述符的JAR称为普通JAR,但这并非官方术语。)

模块系统创建模块运行时镜像所需要的全部信息都包含在模块描述符中。一个模块的所有属性都会在这个文件中呈现;同样,我们讨论的很多特性在这个文件中也有相对应的表述。基于源文件创建这样的描述符(将在下面讲述)并将其包含进JAR,开发者可以手动创建模块,某个工具可以自动创建模块。

虽然包含模块描述符的普通JAR变成了模块化JAR,但它不必强制按照模块化JAR来使用。调用方可以将其放入类路径,把它作为一个普通JAR来使用,并忽略所有与模块相关的属性。这对于逐步模块化现有项目是不可或缺的。(后文将介绍无名模块。)

3 模块声明:定义模块的属性

由此可见,将任何旧的JAR转变成一个模块,唯一需要做的只是为其添加模块描述符module-info.class。这带来了一个问题——如何创建一个模块描述符。就像它的文件扩展名.class所暗示的,它是通过编译源文件获得的。

定义:模块声明

模块描述符由模块声明编译而来。按照约定,模块声明是项目源码根目录中的module-info.java文件。模块声明是模块和模块系统的核心元素。

声明与描述

你可能担心会将术语模块声明和模块描述符弄混。若果真如此,这通常也不是什么大问题。前者是源代码,后者是字节码,它们只是同一个概念的不同形式而已,都表示某个定义模块属性的东西。在特定上下文中通常只有一个合适的选项,所以使用哪种形式一般情况下都是清晰的。

如果这个解释还不能令你满意,并且你希望能确保万全,那么我来分享一下自己的理解:在词典中,声明在描述符之前出现——这很巧妙,因为从时间上讲,你先得到源代码,然后才是字节码。两个顺序是一致的:先得到声明/源代码,然后是描述符/字节码。

模块声明决定了一个模块在模块系统中的标识和行为。后面会介绍的许多特性在模块声明中有相对应的部分,并会在合适的时机呈现。现在看一下JAR缺乏的3个基本属性:名称、明确的依赖以及内部封装。要点 这是一个简单的module-info.java文件的结构,它定义了这3个基本属性。

当然,${module-name}和${package-name}需要被实际的模块名和包名替代。

ServiceMonitormonitor.statistics模块为例。

很容易辨识出前文描述的结构:module关键字后面跟着模块名,主体部分包含了requiresexports指令。下一节将讲述如何声明这3个属性。

新的关键字

module、requires、exports以及后续介绍的一些新的关键字,在一些已有代码中可能已经被用作字段、参数、变量以及其他实体的名称——也许你会好奇这会造成什么影响。很幸运,事实上什么都不需要担心。它们都是限定性关键字,仅在语法期望它们所在的位置上作为关键字。所以虽然不能将变量命名为package或者将模块命名为byte,但是可以将变量甚至模块命名为module

01. 为模块命名

JAR缺少的最基本属性是编译器和JVM可用来标识的名称,因而这是模块最显著的特征。你将有机会甚至有责任为每一个创建的模块命名。

要点 除了module关键字,一个模块声明在最开始要为模块命名。模块的名称必须是一个标识符,这意味着它必须使用与诸如包名相同的命名规则。模块名通常是小写,并且是由“点”分隔的层级结构。

为模块命名是非常自然的事情,因为你平时使用的大多数工具已经要求你为项目命名。但是即使依据项目名称为模块命名是一个可选择的方案,明智的命名选择也是非常重要的!

下面将提到,模块系统强烈地依赖模块的名称。有冲突的或不断变化的名称会造成麻烦,因此以下两个要点对于模块名来说非常重要:

1)全局唯一

2)稳定

最好的命名方式是包命名经常使用的反向域名命名法。在加上标识符的限制后,得到的模块名通常是模块中包的名称前缀。该方式无须强制遵守,但这一点很好地揭示了这两者都是经过慎重选择的。

保持模块名和包名前缀同步,强调了模块名的改变(这暗示了包名的改变)是破坏性最强的改变之一。从稳定性的角度来说,它应该是一个极其罕见的事件。

例如,下面的描述符将模块命名为monitor.statistics(为了让名称简洁,构成ServiceMonitor应用程序的模块不遵循反向域名命名法)。

所有其他属性都在模块名后面的花括号中定义。这里没有规定特别的顺序,但是通常依赖被放置在导出之前。

02. 模块要表明依赖

JAR缺失的另一点是声明依赖的能力。因为无法知道它们正常运行所需要的其他工件,所以人们只能依赖构建工具或文档来获取这些信息。在模块系统中,需要明确指定依赖(如图3-1所示)。

图3-1

为表明模块间依赖引入了JVM可以解释的一层新的抽象。

没有模块间依赖(左),JVM只能看到类型间的依赖;但是有了模块间依赖(右),它就能像人们期望的那样,看到工件间的依赖

定义:依赖

依赖通过requires指令声明,包含了此关键字以及跟在其后的模块名。这个指令陈述,被声明的模块依赖于指定的模块,并在编译和运行时需要它。

monitor.statistics模块在编译时和运行时都依赖于monitor.observer模块。这是通过requires指令声明的。

通过requires指令声明了一个依赖后,如果模块系统无法找到与之完全同名的模块,则会抛出错误。如果缺少依赖模块,不论编译还是启动应用程序都会失败(参见下文)。

03. 导出包以定义模块API

最后是导出指令,它定义了模块的公有API。你可以指定哪些包所包含的类型可被外部模块使用,哪些包只能供内部使用。

定义:导出包

exports关键字后面跟着该模块中一个包的名称。只有导出包可被模块外部使用,所有其他的包都被强封装于模块内部(参见后文)。

monitor.statistics模块导出了一个同名的包。

需要注意,虽然我们倾向于认为包是层级结构的,但其实并非如此!java.util并不包含java.util.concurrent,因此,导出前者并不会公开任何后者包含的类型。这与导入是一致的,importjava.util.*会导入java.util中的所有类型,但不会导入java.util.concurrent中的任何类型(如图3-2所示)。

图3-2

人们倾向于认为包是层级结构的,就像org.junitpioneer包含jupitervintage(左)。但实际上并不是这样。Java只承认完整的包名,并认为二者之间没有任何关系(右)。导出包时必须考虑这个事实,比如exportsorg.junitpioneer不会导出任何jupitervintage中的类型。

04. 模块声明示例

为了实践,先看一下真实世界中的模块声明。最基本的模块是java.base,因为它包含了java.lang.Object,任何Java程序离开它都无法工作。它是所有依赖的顶级依赖:别的模块都依赖它,而它什么都不依赖。

java.base的依赖如此基础,以至于任何模块都不需要明确声明对它的依赖,模块系统会自动将其填充到依赖模块中(更多细节详见下一节)。虽然它什么都不依赖,但是导出了多达116个包,所以此处只能展示一个深度裁剪的版本。

一个更简单的模块是java.logging,它导出了java.util.logging包。

java.rmi是某个模块依赖另一个模块的例子。它会产生日志信息,因此依赖java.logging。它公开的API在java.rmi和以其为前缀的其他包中。

对应用程序ServiceMonitor中的模块进行声明的那些代码尤其如此。

4 模块的众多类型

思考一下目前工作中你正在开发的应用程序。它很有可能包含一系列JAR,而这些JAR在将来的某一时刻都会是模块。

当然,它们并不是应用程序唯一的组成部分。JDK也被拆分成了模块,而这些模块也将进入你需要考虑的范围。但是请等一下,这还并不是全部!由于其中一些模块所具有的特性,因此它们必须被明确地调用。

定义:模块类型

为了避免混乱,下面的术语辨识了不同的模块类型,方便后文更加清晰地讨论模块世界。是时候坐下来掌握它们了。不要担心无法一次性记全。在本页插入书签,方便在遇到任何无法解释的术语时来此查阅。

1)应用程序模块——非JDK模块,Java开发者为自己的项目创建的模块,可以是类库、框架或者应用程序。这些模块存在于模块路径中。目前,它们特指模块化JAR

2) 初始模块——最先开始编译(使用javac命令)的应用程序模块或者包含main函数(使用java命令)的应用程序模块。后文将展示如何借助java命令在启动应用程序时指定初始模块。编译器也依赖这个概念:如同后文中的解释,它指定了最先开始编译的模块。

3) 根模块——JPMS从此处开始解析依赖(后文节将进行详细解释)。除了包含主类或要编译的代码,初始模块同时也是一个根模块。随着对本书的深入阅读,你会遇到一些特殊的状况,需要指定其他模块而非初始模块为根模块(后文将进行解释)。

3) 平台模块——组成JDK的模块,包含Java标准版平台规范所定义的模块(以java.作为前缀)和与JDK相关的模块(以jdk.作为前缀)。如上文中所讨论的,它们被优化存储于运行时libs目录的modules文件中。

4) 孵化模块——非标准的平台模块,名称以jdk.incubator开头。它们包含试验性API,有冒险精神的开发者可以在这些模块正式上线前对它们进行测试。

5)系统模块——除了基于平台模块的子集创建运行时镜像,jlink也可以包含应用程序模块。包含在这种镜像中的平台模块和应用程序模块被共同称为它的系统模块。它们可以通过在镜像的bin目录中执行java --list-modules列出。

6)可见模块——当前运行时的所有平台模块以及通过命令行指定的所有应用程序模块。它们可以被JPMS用来满足依赖。把它们放在一起,就组成了可见模块全集。

7)基础模块——区分应用程序模块和平台模块只是为了让沟通变得更容易。对于模块系统而言,所有的模块都是等同的,只有一个例外:平台模块java.base,即所谓的基础模块。它扮演了一个非同寻常的角色。

平台模块和大多数应用程序模块有模块创建者给予的模块描述符。还有没有其他形式的模块?有。

1)清晰模块——平台模块和大多数应用程序模块有模块创建者给予的模块描述符。

2)自动模块——没有模块描述符的具名模块(剧透:模块路径中的普通JAR)。它们是由运行时而非开发者创建的应用程序模块。

3)具名模块——清晰模块和自动模块的集合。这些模块具有名称,该名称既可以是描述符定义的,也可以是JPMS推断出的。

4)无名模块——没有名称的模块(剧透:类路径中的内容),因此它们不是清晰模块。

自动模块和无名模块都与将应用程序迁移到模块系统的过程相关——这个话题将在后文深入讨论。若要更好地理解这些不同类型的模块之间的关系,请参考图3-3。

图3-3

通过一个简单的图表展示主要模块类型:JDK中的模块称作平台模块,以基础模块为核心;然后是应用程序模块,其中一个必须是初始模块,包含应用程序的main函数(根模块、系统模块和孵化模块没有展示)

再回顾一下探索过的ServiceMonitor应用程序,并将其作为这些术语的例子。它包含7个模块(monitor、monitor.observer、monitor.rest,等等),外加SparkHibernate这样的外部依赖以及相关的传递依赖。

当应用程序启动时,通过命令行指定存放这7个模块以及依赖模块的目录。同运行应用程序的JRE或者JDK中的平台模块一起,它们构成了可见模块全集。模块系统会从这些模块中寻找合适的模块来满足依赖。

ServiceMonitor的各个模块以及它们的依赖模块——HibernateSpark,都属于应用程序模块。因为包含main函数,所以monitor是初始模块,并且不再需要其他根模块。程序唯一直接依赖的平台模块是基础模块java.base,但是HibernateSpark引入了其他模块,比如java.sqljava.xml

由于这是一个全新的应用程序,所有的依赖都是模块化的,因此这并不是一个迁移场景;由于同样的原因,它也没有涉及自动模块和无名模块。

在了解了不同类型的模块以及它们的声明后,是时候探索Java如何处理此信息了。

二 可读性:连接所有片段

模块是模块化应用程序的基石:交互工件图中的节点。但是如果没有连接节点的边,就不能构成图!这就是可读性的意义——基于它,模块系统将为节点之间创建连接。

定义:可读性边

当模块customer声明了需要bar,在运行时customer会读取bar,或反过来,bar将对customer可读(如图3-4所示)。这两个模块之间的连接称作可读性边(readability edge),简称为读取边(readsedge)。

图3-4

customer模块的描述符中声明了对bar模块的依赖。基于此,模块系统会让customer在运行时读取bar像“customer需要bar”和“customer依赖bar”这样的短语反映了customerbar之间的静态编译时关系,可读性则反映了更加动态的、运行时的对应内容。

为什么它更加动态呢?requires指令是可读性边的最初发起者,但这并不意味着它是唯一的发起者,其他发起者还包括命令行选项(参见下文中的--add-reads)以及反射API,二者都可以被用来增加更多的可读性边。最终,requires指令的作用将被弱化。不论可读性边是如何创建的,它们的效果都是一样的——成为可靠配置和可访问性的基础(参见下文)。

1 实现可靠配置

如上文所述,可靠配置旨在保证Java程序编译或启动所用的工件配置正确,而这样的配置可以帮助程序避免运行时错误。为了达到这个目的,它会进行一系列检查(在模块解析过程中,即下文解释的过程)。

要点 模块系统检查可见模块全集是否包含了所有需要的直接依赖或传递依赖,缺少任何依赖都会报错。而且,其中不能有任何歧义,比如不能出现两个工件声称它们是同一个模块的情况。

当某个模块存在不同版本时,这就会变得很有趣:因为模块系统没有版本的概念,它会认为它们是重复模块,所以模块系统会报错。模块之间不能有静态依赖环。

在运行时,模块之间有可能甚至有必要互相访问(比如使用Spring注解的代码,Spring会反射这些代码),但它们之间绝不能有编译依赖(很显然Spring不能对其反射的代码进行编译)。

包必须有唯一的来源,这样就不会出现两个模块分别包含同一个包中的类型的情况。这种情况称为包分裂,模块系统会拒绝编译或启动这样的配置。这对迁移过程而言会非常有趣,因为一些现有的类库或框架会故意对包进行分裂。

这个验证并非万无一失,它有可能让问题隐藏很长时间,使运行中的程序崩溃。例如,如果一个模块的错误版本被放到了正确的位置,那么应用程序会启动(因为所有的依赖模块都存在),但之后在访问某个缺失的类或方法时它会崩溃。

因为模块系统的主旨是在编译时和运行时展现一致的行为,所以可以基于同样的工件来编译和启动,以进一步避免可能的错误。(在例子中,用模块的错误版本进行的编译会失败。)

2 用不可靠配置进行实验

现在试着搞一些破坏。模块系统会检测出哪些不可靠配置?为了便于调查,请回到上面提到的ServiceMonitor应用程序。

01. 依赖缺失

看一下monitor.observer.alpha和它的声明。

在编译时,如果缺失monitor.observer依赖,会抛出如下错误。

如果该依赖在编译时存在,但在启动时丢失,则JVM会退出并报出以下错误。

虽然对于所有传递依赖而言在运行时进行强制检查是有意义的,但对编译器而言并非如此。因此,如果缺少间接依赖关系,编译器既不会发出警告,也不会提示错误,来看以下示例。

下面是monitor.persistence和monitor.statistics的模块声明。

很明显monitor.persistence并不直接依赖monitor.observer,所以即使monitor.observer在模块路径中不存在,monitor.persistence也能编译成功。

缺少传递依赖的应用程序是无法启动的。即使初始模块并没有直接依赖于缺失项,但是只要有模块依赖它,该情况仍然会被报告为依赖缺失。代码库ServiceMonitor中的break-missing-transitivedependency分支创建了一个配置示例,展示了缺失依赖模块而导致运行错误的情况。

02. 重复模块

因为模块是通过名称相互引用的,所以任何情况下,只要两个模块具有相同的名称,就会引起歧义。判断哪一个模块正确非常依赖于上下文环境,而这通常并不是模块系统可以决定的。

因此,为了避免做出错误的决定,模块系统选择不做任何决定,而是产生错误信息。这种快速失败的方式能让开发人员注意到问题并进行修复,而不是放任错误的发生。

下面是一个编译错误。它产生的原因是,模块系统尝试使用模块路径上两个同名的monitor.observer.beta依赖。

请注意,编译器无法将错误指向编译中的某个文件,因为这不是错误的原因。相反,模块路径上的工件才是导致错误的原因。

如果JVM在启动时检测到错误,它会提供更为准确的信息,其中列出了JAR文件名。

正如前文节讨论的那样(后面将深入探讨),由于模块系统没有版本的概念,因此在这种情况下会出现类似的错误。这是一个很好的推测:绝大多数模块重复错误的原因是模块路径上存在同一模块的多个版本。

要点 歧义检查仅适用于单个模块路径的情况。(这句话可能会让你抓耳挠腮——下文会进行详细解释。这里先提一下,以免遗漏这个重要的事实。)

即使没有被引用,只要模块路径中有重复模块,模块系统也会抛出模块重复错误。导致该现象的其中两个原因是服务和可选依赖,后面将详细介绍它们。代码库ServiceMonitor的breakduplicate-modules-even-if-unrequired分支展示了重复模块导致的报错信息,即使该模块没有被引用也是如此。

03. 循环依赖

创建循环依赖很容易,但让它们通过编译很难,要直截了当地把它们呈现给编译器也并非易事。为了做到这一点,必须先解决“鸡生蛋蛋生鸡”的问题:如果两个项目相互依赖,就不可能在缺少一个项目的情况下编译另外一个。如果你尝试过,就会遇到缺少依赖的问题并收到对应的错误提示。

解决这个问题的一种方法是同时编译两个模块,即同时着手于“鸡和蛋”。后面将对此做详细阐述。可以这么说,如果正在编译的模块之间存在循环依赖,那么模块系统会识别出来,并报告编译错误。下面的示例展示了因monitor.persistence和monitor.statistics互相依赖而导致的错误。

另一种方法是,在构建有效配置之后不立即建立循环依赖,而是随着时间推移逐渐建立。再次回到monitor.persistencemonitor.statistics

这个配置是正确的,而且能编译成功。接下来,奇妙的事情发生了:编译模块并保留JAR,然后将monitor.statistics中的模块声明更改为依赖monitor.persistence,这会创建一个循环依赖(此示例中的更改没有多大意义,但在更复杂的应用程序中通常会这样做)。

下一步是在模块路径上将已编译过的模块和更改后的monitor.statistics一块重新编译。这其中肯定包含了monitor.persistence,因为monitor.statistics模块现在依赖它。相反,monitor.persistence模块依然声明了它对monitor.statistics的依赖,这就是这个循环依赖的后半部分。很遗憾,一番操作之后,模块系统还是发现了循环依赖问题,并抛出了与之前相同的编译错误。

现在是时候展现真正的技术了:用一个更高明的手段来“欺骗”编译器。在本场景中,要用两个完全不相关的模块——比如选择monitor.persistence和monitor.rest——各自编译成JAR,之后就是关键部分。

新增第一个依赖,比如使persistence依赖rest,并且更改后的persistence基于原有的模块集进行编译。它能编译成功,因为原有模块集中的rest并不依赖persistence

新增第二个依赖,即使rest依赖persistence,但是让更改后的rest也基于原有的模块集编译,其中包含的persistence模块是修改前的、尚未依赖rest的版本,因此也能编译成功。

是不是把你弄糊涂了?那么看一下图3-5,它从另外一个视角进行了解读。

图3-5

让循环依赖通过编译并不容易,这里通过选择两个互不依赖的模块:persistencerest(均依赖于statistics),然后分别添加从一个到另外一个的依赖来达到目的,其中最重要的是基于旧的persistence来编译rest,这样不会有循环依赖的问题,编译可以通过。

在最后一步中,两个旧的模块都可以用新编译的模块替换,而新模块之间具有循环依赖关系于是现在,我们有了monitor.persistencemonitor.rest模块的不同版本,它们之间相互依赖。

如果这一切发生在现实世界中,那么编译过程(可能由构建工具管理)一定会产生严重的混乱(这并非前所未闻)。幸好,当在这种配置环境下启动JVM后,模块系统会帮助你并报告如下错误。

尽管所有示例展示的都是两个工件之间的循环依赖关系,但是模块系统会进一步检测所有循环依赖关系。这确实很棒!代码变更总有破坏上游功能的风险,因为上游功能中的代码可能直接或间接地调用了被改动的代码。

如果依赖关系只在一条直线上延续,那么发生改变时只会影响这条线上的代码。与此相应的是,如果依赖关系形成了一个循环,那么该循环中的所有代码以及依赖于它的所有代码都会受到影响。

特别是如果循环范围非常大,那么很快所有代码都会受到影响,不必说,这种情况一定要避免。幸好,不只模块系统能帮助你,构建工具也可以,它也是处理循环依赖的好帮手。

04. 包分裂

当两个模块在同名包中含有相同的类型时,就会发生包分裂(splitpackage)。举个例子,还记得monitor.statistics模块的monitor.statistics包中包含Statistician类吧。现在假设monitor模块中有一个它的简单实现SimpleStatistician。为了保持一致性,该实现位于monitor的monitor.statistics包中。

当尝试编译monitor模块时,编译器会提示如下错误。

要点 有趣的是,只有当编译中的模块可以访问另一个模块中分裂的包时,编译器才会提示错误。这表明分裂的包必须被导出。

现在尝试另外一种方式:假设SimpleStatistician不再存在,并且这回让monitor.statistics创建分裂的包。为了方便复用一些工具方法,在monitor包中创建Utils类。由于不希望与其他模块共享该类,因此模块仍然只导出monitor.statistics包。

于是我们可以顺利地编译monitor.statistics,这是有道理的,因为它不依赖于monitor,因此也就不会意识到包分裂。当编译monitor的时候事情变得有趣了,它依赖于monitor.statistics,并且二者的monitor包中都包含同样的类型。但是,就像前文提到的,因为monitor.statistics没有导出相关的包,所以编译通过。

好极了!是时候启动它了。

情况好像还是不太对。在启动时,模块系统会检查包分裂,而且该检查与这些包是否被导出无关:不允许两个模块包含同一个包中的类型。正如你将在后文节中看到的那样,这会导致将代码迁移到Java9时出现问题。

ServiceMonitor代码库分别展示了编译时和运行时的包分裂问题,对应的分支是break-split-package-compilationbreaksplit-package-launch

05. 模块的死亡之眼

一个非常极端的融合了包分裂和依赖缺失的场景称为模块的死亡之眼(modular diamond of death)(如图3-6所示)。假设某个模块在新版本中修改了名称,即一个依赖通过旧名称引用该模块,而另一个依赖通过新名称引用该模块。现在,你需要让相同的代码显示在两个不同的模块名称下,然而JPMS是不会让这种情况发生的。

图3-6

如果一个模块更改了名称(如图,从jackson改为johnson),那么依赖两个版本的项目(如图,项目app分别通过frameborder产生依赖)最终可能会面临模块的死亡之眼,因为它们依赖于同一个项目,这个项目却有着两个不同的名称你会遇到以下两种情况之一。

1) 一个模块化JAR只能以一个名称作为模块出现。因此无法满足依赖,从而导致错误。

2) 两个模块化JAR具有不同的名称却包含相同的包,这将导致上文中提到的包分裂错误。

要点 应该不惜一切代价避免这种情况!如果要发布某个工件到公共仓库,你应该仔细考虑是否有必要对模块进行重命名。如果有必要,你可能还需要修改包名,以方便其他人同时使用新旧模块。如果以用户的身份遇到这种情况,那么你可以创建聚合器模块来解决这一问题。

标签: #隐藏模块的编译错误是什么意思 #隐藏模块的编译错误是什么意思呀 #隐藏模块的编译错误是什么意思呀视频 #隐藏模块的编译错误是什么意思呀怎么解决 #java中的mod是什么意思