阅读更多
1 聚合
有时候,我们想要一次构建多个项目,而不是到这些模块的目录下分别执行mvn命令。Maven聚合(或者称多模块)这一特性就是为该需求服务的
对于聚合项目,其POM文件的<packaging>
元素必须是pom,否则无法构建
一个聚合项目,其POM文件包含如下几个重要元素,详见下方示意代码
<packaging>
:打包方式,必须为pom<name>
:提供一个更易阅读的名字,会在构建时显示<modules>
:聚合项目的核心配置- 每个
<module>
元素的值都是一个当前POM的相对目录(很重要)
- 每个
1 | <project> |
一般来说,为了方便快速定位内容,模块所处目录的名称应当与其artifactId一致。当然也可以不一样,只不过需要修改聚合POM文件中的<module>
元素的值
此外,为了方便用户构建项目,通常将聚合模块放在项目目录的最顶层,其他模块则作为聚合模块的子目录存在。这样当用户得到源码的时候,第一眼发现的就是聚合模块的POM,不用从多个模块中去寻找聚合模块来构建整个项目
2 继承
在面向对象的世界中,程序员可以使用类继承在一定程度上消除重复,在Maven的世界中,也有类似的机制能让我们抽取出重复的配置,这就是POM的继承
对于一个父模块,其POM文件包含如下几个重要元素,详见下方示意代码
<packaging>
:打包方式,必须为pom,这一点与聚合模块一样<name>
:提供一个更易阅读的名字,会在构建时显示
1 | <project> |
对于子模块,其POM文件包含如下几个重要元素,详见下方示意代码
<parent>
:用于声明父模块<groupId>
、<artifactId>
、<version>
:父模块的坐标,必须指定<relativePath>
:当前POM的相对目录,用于定位父模块pom文件的目录- 在项目构建时,Maven会首先根据relativePath检查父POM,如果找不到,再从本地仓库找
- relativePath的默认值是:
../pom.xml
,也就是默认父POM在上一层目录下
2.1 可继承的POM元素
<groupId>
:项目组ID,项目坐标核心元素<version>
:项目组版本,项目坐标核心元素<description>
:项目的描述信息<organization>
:项目的组织信息<inceptionYear>
:项目的创始年份<url>
:项目的URL地址<developers>
:项目的开发者信息<contributors>
:项目的贡献者信息<distributionManagement>
:项目的部署配置<issueManagement>
:项目的缺陷跟踪系统信息<ciManagement>
:项目的持续集成系统信息<scm>
:项目的版本控制系统信息<mailingList>
:项目的邮件列表信息<properties>
:自定义的Maven属性<dependencies>
:项目的依赖配置<dependencyManagement>
:项目的依赖管理配置<repositories>
:项目的仓库配置<build>
:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等<reporting>
:包括项目的报告输出目录配置、报告插件配置等
2.2 依赖管理
Maven提供的<dependencyManagement>
元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性
在父模块的POM文件中的<dependencyManagement>
元素下的依赖声明不会引入实际的依赖(父子模块都不会引入实际的依赖),不过它能够约束<dependencies>
下的依赖使用
<dependencyManagement>
元素是可以被继承的,但是并不会引入实际的依赖。因此在子模块的POM文件中,还是需要在<dependencies>
元素中声明所需的依赖,但是只需要配置依赖的groupId以及artifactId即可,详见下方示意代码
父POM文件
1 | <project> |
子POM文件
1 | <project> |
可以看到,子POM文件中的依赖配置较原来简单了一些,所有的springframework只配置了groupId和artifactId,省去了version;而junit不仅省去了version还省去了scope。这是因为完整的依赖声明已经包含在父POM中,子模块只需要配置简单的groupId和artifactId就能获得对应的依赖信息,从而引入正确的依赖
这种依赖管理机制似乎不能减少太多的POM配置,不过还是建议采用这种方法,原因如下
- 父POM中使用
<dependencyManagement>
声明依赖能够统一项目范围中依赖的版本 - 当依赖版本在父POM中声明之后,子模块在使用依赖的时候就无需声明版本,也就不会发生多个子模块使用依赖版本不一致的情况,这可以帮助降低依赖冲突的概率
此外,Maven-Basics中提到了名为import的依赖范围,该范围的依赖只在<dependencyManagement>
元素下才有效果,使用该范围的依赖通常指向一个POM,作用是:将目标POM中的<dependencyManagement>
配置导入并合并到当前POM的<dependencyManagement>
元素中
2.3 插件管理
类似的,Maven也提供了<pluginManagement>
元素帮助管理插件,该元素中配置的依赖不会造成实际的插件调用行为。由于<build>
元素可被继承,因此其子元素<pluginManagement>
也可以被继承。在子POM文件中配置了真正的<plugin>
元素,其groupId与artifactId与父POM文件中的<pluginManagement>
元素中配置的插件匹配时,<pluginManagement>
的配置才会起作用
同样的,完整的插件声明已经包含在父POM中,子模块只需要配置简单的groupId和artifactId就能获得对应的插件信息,从而引入正确的插件
父POM文件
1 | <project> |
子POM文件
1 | <project> |
3 聚合与继承的关系
聚合主要是为了方便快速构建项目,而继承主要是为了消除重复配置。聚合和继承是两个正交的概念
对于聚合模块来说,它知道有哪些被聚合的模块,但那些被聚合的模块不知道这个聚合模块的存在
对于继承关系的父POM来说,它不知道有哪些子模块继承于它,但那些子模块都必须知道自己的父POM是什么
聚合和继承唯一的共性是其<packaging>
元素的值都是pom
在现有的项目中,往往将聚合与继承合二为一,即一个POM既是聚合POM又是父POM,这么做主要是为了方便
4 约定优于配置
Maven会假设用户的项目是这样的:
- 源码目录为:
src/main/java/
- 编译输出目录为:
target/classes/
- 打包方式为:
jar
- 包输出目录为:
target/
遵循约定虽然损失了一定的灵活性,用户不能随意安排目录结构,但是却能减少配置。更重要的是,遵循约定能够帮助用户遵守构建标准
没有约定,意味着10个项目可能使用10种不同的项目目录结构,这意味着交流学习成本的增加,而这种增加的成本往往就是浪费
任何一个Maven项目都隐式地继承自超级POM
($MAVEN_HOME/lib/maven-model-builder-x.x.jar
中的org/apache/maven/model/pom-4.0.0.xml
),这有点类似于任何一个Java类都隐式地继承于Object类。因此大量超级POM的配置都会被所有Maven项目继承,这些配置也就成了Maven所提倡的约定
5 反应堆
在一个多模块的Maven项目中,反应堆(Reactor)是指所有模块组成的一个构建结构
- 对于单模块项目,反应堆就是该模块本身
- 对于多模块项目,反应堆就包含了各模块之间继承与依赖的关系,从而能够自动计算出合理的模块构建顺序
实际的构建顺序是这样的:Maven按序读取POM,如果该POM没有依赖模块,那么就构建该模块,否则就先构建其依赖模块,如果该依赖还依赖于其他模块,则进一步先构建依赖的依赖
模块间的依赖关系会将反应堆构成一个有向非循环图(有向无环图的遍历,BFS与DFS均可实现),若出现了环状依赖,那么Maven在构建时会报错
5.1 裁剪反应堆
Maven提供很多命令行选项支持裁剪反应堆,输入$mvn -h
可以看到以下选项
-am,--also-make
:同时构建所列出的依赖模块-amd,--also-make-dependents
:同时构建依赖于所列模块的模块-pl,--projects <arg>
:构建指定模块,模块间用逗号分隔-rf,--resume-from <arg>
:从指定的模块回复反应堆
6 参考
- 《Maven实战》