Maven无法生成模块声明 ,要为Java9的模块系统声明模块,我们需要创建一个文件module-info.java——所谓的模块声明。除其他事项外,它声明对其他模块的依赖性。但是,如果项目使用构建工具,例如Maven,它已经管理了依赖项。保持两个文件同步肯定看起来多余且容易出错,所以问题出现了:“Maven不能为我生成模块信息文件吗?” 不幸的是,答案是“否”,原因如下。

1.动机

那么大家就疑惑了,Maven无法生成模块声明吗?原因很明显:如果我向POM添加依赖项,它很可能也是一项要求。或者反过来:如果我向声明添加需求,则可能还必须向POM添加匹配的依赖项。听起来像是可以自动化的东西。

让我们从尝试从需求回到依赖关系开始:由于缺乏信息,这是不可能的。模块名称无法转换为Maven坐标,缺少groupId和artifactId等信息。还有:选择哪个版本?模块声明对路径上工件的哪个版本不感兴趣,只关心它是否可用。

另一方面,从依赖文件到模块名称是可能的。但是,这还不足以生成模块声明的所有元素。

2.关注点分离

在谈论这个话题时,涉及三个实体:Maven的POM、模块系统的模块信息文件,以及参与构建类和模块路径的Maven插件。这三者一起使使用模块成为可能,但它们都有自己的职责。

3.POM依赖项

依赖项的任务是(1)根据坐标获得对文件的唯一引用,以及(2)指定何时使用它。何时使用它部分由scope和控制,基本上是compile、test和的任意组合runtime。

坐标至少是、和文件扩展名的组合groupId,artifactId它version源自type.classifier也可以选择添加一个。基于此信息,可以引用本地存储库中的文件。它看起来像下面这样,其中groupId中的每个点都替换为斜线:

${localRepo}/${groupId}/${artifactId}/${version}/${artifactId}-${version}[-${classifier}].${ext}

如您所见,您可以引用任何文件:文本文件、图像、可执行文件。在依赖关系的上下文中,这无关紧要。除此之外,依赖项不知道文件的内容。例如,如果是JAR:它是为Java8还是Java1.4构建的,如果您的项目必须与相当旧的Java版本兼容,这可能会产生很大的不同。

4.模块声明

模块声明文件由五个声明组成:

requires
必须可用于编译或运行此应用程序的模块。
requires
其类型对少数或所有模块可见的包。
opens
其类型。
opens
该模块可能发现的服务接口。
provides
为特定服务接口提供的实现。

从这些声明中,requires子句与Maven项目的依赖关系密切相关。如果JDK/JRE连同Maven提供的依赖文件不能满足这些要求,项目将无法编译或运行。将其视为必须遵守的质量规则。

5.Maven 插件

每个插件(或者实际上是插件目标)都可以指定依赖项的解析范围并访问这些文件。例如,maven-compiler-plugin的编译目标声明它使用编译时依赖项解析。这个插件根据Maven提供的依赖文件和插件的配置为Java编译器构造正确的参数。

6.类路径和模块路径

在Java8之前,它非常简单:所有JAR都需要添加到类路径中。但是对于Jigsaw,JAR可以在模块路径或类路径中结束。这完全取决于另一个模块是否需要JAR。重要的是要意识到我们必须在100%的时间里做到这一点,因为如果一个工件最终走上了错误的路径,模块系统将拒绝配置和编译或启动将失败。

假设我们已经为我们的项目编写了一个模块声明。我们必须在哪里指定JAR是否是必需的模块?换句话说,它属于类路径还是模块路径?一个明显的位置是POM中的依赖项。但是,有几个原因导致这不起作用。

7.不用担心

首先,它不是依赖关系的关注点。依赖关系是关于对文件的引用以及何时使用它(编译时?运行时?)而不是如何使用。

8.POM 中没有位置

然后,从依赖项的所有元素中,只有一个可用于控制JAR的更细粒度的使用:scope.然而,POM的模型版本4.0.0有一组严格的范围。虽然POM中充满了Maven的构建指令,但一旦安装或部署,它也是一个元文件,其中包含其他构建工具和IDE的依赖信息,因此新范围可能会混淆或破坏此类产品。

出于同样的原因,为依赖项引入新的XML元素也会有问题;它会与XSD冲突,其他工具还没有为它准备好。因此,在POM的依赖声明中没有用于模块化信息的空间,并且在短期内,不应期望仅针对Jigsaw的新POM定义。

那么如何使用maven-compiler-plugin配置它呢?

<plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <!-- <version>x.y.z</version> -->
        <configuration>
                <!-- example -->
                <modulePath>
                        <!-- by dependency? -->
                        <dependency>com.foo.bar:library</dependency>
                        <!-- by module? -->
                        <module>library</module>
                </modulePath>
        </configuration>
</plugin>

但是传递依赖性呢,在这种情况下是library的依赖性?这些依赖关系也需要在两条路径上进行划分。你需要配置吗?

或者,我们可能希望解析依赖项的构建信息以推断将其依赖项放置在何处,但这并不可行,因为此类信息并不总是存在。即使这样的JAR是使用Maven构建的,试图将JAR中的内容逆向工程到有效的插件配置通常也是不可能的(想想多个执行块的影响)。

9.读取模块描述符

因此,如果POM不是可行的方法,我们还能从哪里获得信息?最好按原样关注JAR中的文件,例如module-info.class…(编译后的模块声明,称为模块描述符)。

所以密钥文件似乎是module-info文件,因为它准确地指定了要求。一旦像ASM这样的工具可以读取模块描述符,maven-compiler-plugin就可以提取该信息并决定每个依赖项是否与所需的模块匹配。对于我们的项目,应该有一个module-info.java让Maven知道在哪里放置我们的依赖项。为此,需要在编译之前对其进行分析,这已经可以通过QDox实现。

因此,module-info.class有了JAR中的所有文件和module-info.java我们项目的文件,就可以确定每个JAR所属的位置;类路径或模块路径。这使得模块声明和描述符成为成功构建的要素。但问题仍然存在,难道它不也是一个结果吗?

10.模块声明生成器

在分析该项目中使用的所有模块描述符时,我们发现应该可以将所有JAR划分到模块路径和类路径上。但是是否可以为我们的项目生成模块声明?

对于中的要求,src/main/java/module-info.java我们可以走得很远。我们可以说所有具有范围的依赖项compile或provided都是要求,test当然不是,但是对于runtime(这意味着仅在运行时)它取决于。但也有未映射到JAR的要求。相反,它们指的是java或jdk模块(例如java.sql,java.xml或jdk.packager)。必须为生成器配置这些,否则需要预先分析代码。

需求也可以有额外的修饰符。使用requiresstatic子句,您可以指定模块在编译时是必需的,但在运行时是可选的。在Maven中,你会将这样的依赖标记为optional.使用transitive修饰符指定使用此模块的项目不必在其自己的模块声明文件中添加传递模块。所以transitive对这个项目的影响为零,它只会帮助其他项目使用这个作为依赖项。此类信息只能作为配置提供。

即使重新设计POM定义以支持这种依赖元数据以便转换为模块声明文件中的需求,也有更多的声明,更难生成。它已经从模块本身开始:它的名字是什么,它是否打开?

导出和打开的包又如何呢?看起来好像可以通过代码分析推断出它们。不过,目前还不可能在此细节上将java文件作为源进行分析。将class文件解析为二进制文件意味着双重编译,首先仅使用类路径获取所有类进行分析,然后同时使用模块路径和类路径,才是真正的编译。没有可行的解决方案。

最后,模块声明中的每一行都是开发人员的选择。向POM添加配置是否有意义,它看起来很像预期的模块声明文件?

我们能得到的最接近的是使用如下模板。让我们使用Velocity并调用模板module-info.java.vm。

open module M.N
{
     #set ( $requiredModules = ... ) ## these must somehow be available in the context

     ## requires {RequiresModifier} ModuleName ;
     #set( $transitiveModules = ["org.acme.M1", "org.acme.M2"] )
     #foreach( $requiredModule in ${requiredModules} )
         #if( $transitiveModules.contains( $requiredModule.name ) ) transitive #end #if( $requiredModule.optional ) static #end ${requiredModule.name} ;
     #end

     requires java.sql;
     requires java.xml;
     requires jdk.packager;

     ## exports PackageName [to ModuleName {, ModuleName}] ;
     exports com.acme.product;
     exports com.acme.service;

     ## opens PackageName [to ModuleName {, ModuleName}] ;

     ## uses TypeName ;

     ## provides TypeName with TypeName {, TypeName} ;
}

将其转换为module-info.java可能需要一个单独的模板maven-plugin,因为这超出了maven-compiler-plugin的编译任务和maven-resource-plugin的带有可选过滤任务的资源复制。与普通的旧(新)模块声明文件相比,这种带有一些脚本语法的模板是否更易于阅读和维护,这取决于开发人员。

有些人可能听说过jdeps,这是一种从JDK8开始可用的工具,可以分析依赖关系。它还可以选择生成模块声明文件,但不是我们想要的方式。jdeps只使用编译类,所以必须在phase之后使用。compile引入此功能是为了让开发人员从一个module-info.java文件开始,但一旦生成,就由开发人员调整和维护它。

11.结论

如果有一个模块声明生成器,它仍然需要相当多的配置才能得到恰到好处的结果文件。这不会比直接写入文件少工作。总之,自己编写和维护模块声明可以保证它始终如您所愿。

当然,一些小型开源项目会弹出并尝试生成模块声明,但它只适用于一部分项目,这意味着我们不能为Maven公开它。

我更希望IDE能为您解决这个问题。例如,如果您向POM添加依赖项,您也可以选择将其作为要求添加到模块声明中。或者他们提供了一个向导来显示项目中的所有依赖项和包,让您可以选择将什么添加到模块声明文件中。

不管故事的那一部分如何发展,尽管如此,我在这里告诉您,暂时Maven不会为您编写模块声明。

12.推荐阅读

JavaScript中的三元运算符-极速阅读

Python异常处理