Maven-Single-Module-Compilation
最近工作比较繁忙,没有太多时间学习和写笔记,忙里偷闲写下了这篇。
在大型Java
项目中,通常会有多个主应用。主应用是指具有诸如public static void main
程序入口,可以被java
执行的模块。Maven
概念中一个packing
属性不为pom
的pom
文件对应一个模块,可以单独编译、测试和打包。与应用相对的概念是库,不包含程序入口,但可以提供特定功能和服务被其他应用调用。在一个项目中,不同主应用之间独立运行又相互协作。例如常规SpringBoot
应用提供页面操作接口,可以添加自动任务,而自动任务应用间隔扫描任务列表,并根据任务状态来执行对应操作。
当修改代码的范围只影响项目中一个应用,只需打包部署对应应用及其依赖即可。
在IDEA
的Maven
菜单界面中,有列出项目中的所有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 |
|
-pl
参数指定了编译的具体模块。指定后maven
命令不再对所有模块执行。-am参数指定了目标模块如果依赖项目中其他模块,那会递归编译。
如果目标应用pom
和根pom
在文件目录上不是直接父子目录关系,通常使用[groupId]:artifactId
的方式来指定具体模块。通常[groupId]
可以省略,所以常见的写法是。
1 |
|
把some-app
替换为具体目标应用的groupId
即可。
总结
maven
中项目继承和项目聚合是两个非常常用的功能。但由于都涉及父子关系的概念,很容易将两者混淆。
项目继承是指子pom
通过
<parent>
标签指定父pom
。这样子pom
就可以从父pom
中获得很多属性的默认值。例如在依赖管理中,可以在父pom
中指定依赖的版本,在子pom
中就不需要指定依赖的版本号。
项目聚合是指在一个父pom
中指定其拥有的模块,并将<packaging>
标签属性设置为pom
。此后在父pom
中调用maven
命令时,默认会在所有子模块中递归执行。执行顺序取决于子模块间的依赖关系。
项目聚合默认会对所有子模块执行,如果只相对其中某个应用执行,则可以通过-pl am
参数来指定目标模块。