Maven-Single-Module-Compilation

最近工作比较繁忙,没有太多时间学习和写笔记,忙里偷闲写下了这篇。

在大型Java项目中,通常会有多个主应用。主应用是指具有诸如public static void main程序入口,可以被java执行的模块。Maven概念中一个packing属性不为pompom文件对应一个模块,可以单独编译、测试和打包。与应用相对的概念是,不包含程序入口,但可以提供特定功能和服务被其他应用调用。在一个项目中,不同主应用之间独立运行又相互协作。例如常规SpringBoot应用提供页面操作接口,可以添加自动任务,而自动任务应用间隔扫描任务列表,并根据任务状态来执行对应操作。

当修改代码的范围只影响项目中一个应用,只需打包部署对应应用及其依赖即可。

IDEAMaven菜单界面中,有列出项目中的所有pom文件。如果选择其中某个主应用执行mvn clean package,则可能会提示无法找到依赖的项目中的其他模块,或者打包后发现受影响的依赖库没有更新。

解决问题最简单的方法是在所有模块的根pom执行mvn clean package,这样所有的模块都会进行编译。当然会得到正确的jar包,但不受影响的库和应用也进行了重新编译,增加了编译过程的耗时。

会出现这种问题的原因是,模块之间并不互相认识,因此在编译时无法区分其依赖是项目中的其他模块,还是在本地库中的第三方模块。为了说明这个情况,需要了解项目继承项目聚合

项目继承

项目继承(Project Inheritance)官方文档

通俗来说,项目继承就是在pom文件中,通过 <parent>标签指定父pom。具体可以通过artifactId或相对路径来指定。父pom可以来自于同一个项目,也可以来自其他第三方pom。如果来自于同一项目且使用artifactId时,则需要父pom文件位于子pom的上一级目录。

项目继承使得子pom可以继承父pom中的标签属性值。当然也不是所有的属性都会被继承,可以被继承的属性可以参考官方文档。

Elements in the POM that are merged are the following:

  • dependencies
  • developers and contributors
  • plugin lists (including reports)
  • plugin executions with matching ids
  • plugin configuration
  • resources

这意味着如果在子pom中,不显式地给某个属性赋值时,会默认使用父pom中的值。常见的作用是依赖管理。如果项目中多个模块会使用到同一个第三方库,依赖的版本不一致可能会出现问题。此时在父pom中进行了依赖版本的设定,那么在子pom中无需指定依赖的版本,会默认使用父pom中设定的版本。这样可以在单个pom文件中,进行整个项目用到的第三方库的版本管理。

项目聚合

项目聚合(Project Aggregation)官方文档

通俗来说,项目聚合就是在父pom文件中,通过<modules>标签指定其包含的模块。当在父pom处调用maven命令,如mvn clean install,则会递归地在其所有模块中调用相同命令。在项目根pom中调用可以编译所有模块使用的就是这个机制。此外还需要将父pom<packaging>标签属性设置为pom

值得一提的是,虽然使用了父pom这个概念,但项目聚合本身不必须进行项目继承。也就是在子pom中不必须指定<parent>。但通常项目聚合和项目继承会一起使用。其中项目继承常用来控制版本,项目聚合用来管理不同模块的递归编译。

问题原因

在主应用所在的pom处执行mvn clean package,由于其没有子模块,因此不符合项目聚合的条件。所以并不知道其依赖来自于同一个项目,还是本地库。默认所有依赖都会去本地库中查找。如果本地库中存在同一个项目的模块上次打包的jar包,则会使用这个jar包,否则则会报错无法找到依赖。由于上次打包的jar包并不包含项目中现有的代码变动,因此会产生明明代码改了但没生效的情况。

一个直观的想法便是,在编译某个主应用前,手动将所有其他依赖的项目中的模块重新打包安装到本地库。确实可以解决问题,但这样会麻烦,而且模块之间的依赖和依赖传递关系靠手动分析很容易出错。

这个问题显然和项目聚合相关,而不是项目继承。出现这个问题的原因是,项目聚合只能在父pom处调用。父pom可以通过递归访问子模块信息,来组织子模块间的依赖关系。子pom无法通过递归访问父pom来了解父pom的其他模块信息。前文提到项目聚合不必须进行项目继承,也就是子pom可能都不知道父pom是谁,父pom有没有自己这个孩子。也就是说项目聚合的递归调用只能是从根到子节点,而不能是子节点向根(pom通过树的形式组织)。子pom所在的模块,项目中的其他模块对其来说是不可见的。

解决方法

要能满足只编译主应用及其依赖模块,根据项目聚合的特性,只能在主应用和依赖模块的共同祖先节点处执行maven命令。不管项目具体结构如何,根目录一定满足这个共同祖先这个要求。所以为了解决这个问题通常还是会在根pom处执行maven命令。

而在根pom直接执行maven命令,默认行为会对所有模块执行。因此maven提供了一组参数来控制这个过程。

1
2
3
4
-pl, --projects
Comma-delimited list of specified reactor projects to build instead of all projects. A project can be specified by [groupId]:artifactId or by its relative path
-am, --also-make
If project list is specified, also build projects required by the list

-pl参数指定了编译的具体模块。指定后maven命令不再对所有模块执行。-am参数指定了目标模块如果依赖项目中其他模块,那会递归编译。

如果目标应用pom和根pom在文件目录上不是直接父子目录关系,通常使用[groupId]:artifactId的方式来指定具体模块。通常[groupId]可以省略,所以常见的写法是。

1
mvn clean install -pl :some-app -am

some-app替换为具体目标应用的groupId即可。

总结

maven中项目继承和项目聚合是两个非常常用的功能。但由于都涉及父子关系的概念,很容易将两者混淆。

项目继承是指子pom通过 <parent>标签指定父pom。这样子pom就可以从父pom中获得很多属性的默认值。例如在依赖管理中,可以在父pom中指定依赖的版本,在子pom中就不需要指定依赖的版本号。

项目聚合是指在一个父pom中指定其拥有的模块,并将<packaging>标签属性设置为pom。此后在父pom中调用maven命令时,默认会在所有子模块中递归执行。执行顺序取决于子模块间的依赖关系。

项目聚合默认会对所有子模块执行,如果只相对其中某个应用执行,则可以通过-pl am参数来指定目标模块。


Maven-Single-Module-Compilation
http://dracoyus.github.io/2023/07/18/Maven-Single-Module-Compilation/
作者
DracoYu
发布于
2023年7月18日
许可协议