0%

Maven(四): 聚合与继承

Maven不仅支持单模块项目同时还支持多模块项目,这里将介绍在多模块Maven项目中广泛应用的聚合、继承等概念,并就如何通过裁剪反应堆来加快构建过程进行介绍

abstract.png

聚合

对于复杂的Maven项目,一般建议采用多模块的方式来设计开发,便于后期维护管理。但是构建项目时,如果每次都需要按模块一个一个进行构建会十分麻烦,而Maven的聚合功能就可以很好的解决这个问题,当用户对聚合模块执行构建任务时,会对所有被其聚合的模块自动地依次进行构建任务

这里通过一个实例进行介绍,在manager模块中有 manager-mapper、manager-service、manager-controller、manager-pojo 四个模块,为了避免依次构建这四个模块,我们这里添加了一个新的模块manager用于将这四个模块聚合在一起,通常我们称manager为聚合模块。从下图实例我们可以看出,聚合模块不包含任何源码,其只有一个POM文件。因为该模块的作用只是为了聚合其他模块方便我们一次性构建其下的所有模块,所以其POM文件的内容与一般的POM文件有所差别。首先,将打包方式packaging元素设置为pom,然后通过modules下的module子元素来添加该聚合模块需要聚合的模块的目录路径

figure 1.jpeg

现在,我们在聚合模块manager下执行构建任务,即可看到其所聚合的四个聚合将会全部执行,大大方便我们构建项目

figure 2.jpeg

实例中,聚合模块和被聚合模块之间的目录结构是父子目录结构,实际上其还可以是平行目录结构,如下图所示

figure 3.png

但如果使用平行目录结构下,聚合模块的POM文件中的被聚合模块的目录路径需要进行修改,因为module元素的值是基于当前POM文件的相对目录

1
2
3
4
5
6
<modules>
<module>../manager-mapper</module>
<module>../manager-service</module>
<module>../manager-controller</module>
<module>../manager-pojo</module>
</modules>

继承

概述

在一个多模块的项目中,对于同一个依赖的依赖声明要在多个模块的POM都进行声明,会导致有大量重复的依赖声明。所以Maven在设计之时,借鉴了面向对象中的继承思想,可在父模块的POM中声明依赖,子模块的POM文件可通过继承父模块的POM来获得对相关依赖的声明。对于父模块而言,其目的是为了消除子模块的POM文件的重复配置,其不含有任何实际的项目代码,所以父模块POM文件的packaging元素同样需要设置为pom,如下所示

figure 4.jpeg

而在子模块的POM文件中,通过parent元素声明对父模块的POM文件的继承,parent元素中的relativePath子元素用来指明父模块POM的路径,该值默认值为../pom.xml。其值同样是一个是基于当前POM文件的相对路径。所以对于使用父子目录结构的实例的api模块而言,该元素值可省略不用显式指定,使用默认值即可

figure 5.jpeg

而如果使用平行目录结构,则在api模块的POM中的relativePath元素应配置为../springBoot2-root/pom.xml

figure 6.png

Maven可通过继承获得POM元素,列表如下:

  • groupId:项目组ID,项目坐标的核心元素
  • version:项目版本,项目坐标的核心元素
  • description:项目的描述信息
  • organization:项目的组织信息
  • inceptionYear:项目的创始年份
  • url:项目的URL地址
  • developers:项目的开发者信息
  • contributors:项目的贡献者信息
  • distributionManagement:项目的部署配置
  • issueManagement:项目的缺陷跟踪系统信息
  • ciManagement:项目的持续集成系统信息
  • scm:项目的版本控制系统信息
  • mailingLists:项目的邮件列表信息
  • properties:自定义的Maven属性
  • dependencies:项目的依赖配置
  • dependencyManagement:项目的依赖管理配置
  • repositories:项目的仓库配置
  • build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等
  • reporting:包括项目的报告输出目录配置、报告插件配置等

Note :

在子模块POM文件的parent元素中声明父模块的版本时,建议引用父模块的属性,而不要直接在各个子模块写父模块的版本号。以避免父模块版本号变动后,在对各个子模块的POM进行更新时因遗漏而出现错误。有时候,在IntelliJ IDEA中会报错: parent元素中不可引用属性,只需在父模块中用revision属性名定义其版本号即可消除报错

1
2
3
4
5
6
7
...
<properties>
...
<revision>1.0</revision>
...
</properties>
...

dependency Management 依赖管理

由上文可知,子模块可通过继承获得父模块中声明的dependencies元素——即全部依赖,这样虽然避免了在各个子模块POM中重复写依赖声明,但是这样也会造成另外一个问题——会导致子模块中引入了不必要的依赖。显然这个也不是我们期望的,为此Maven引入了dependencyManagement来对依赖进行管理。该元素下的依赖声明不会实际引入到模块中,只有在dependencies元素下同样声明了该依赖,才会引入到当前模块中。dependencies声明的依赖若未指定版本号,将使用dependencyManagement中该依赖指定的版本;否则,将覆盖dependencyManagement中该依赖指定的版本

figure 7.jpeg

所以较好的实践是,我们在父模块POM中通过dependencyManagement元素声明子模块需要使用的依赖及其版本。由于dependencyManagement元素同样可被继承。故我们在各个子模块POM的dependencies元素下声明实际需要用到的依赖。这样看上去虽然不能减少子模块的POM配置。但是其一方面可以在父模块的POM中统一管理子模块用到的依赖的版本(更进一步地,dependencyManagement中元素的版本亦支持引用属性),另一方面也不至于将不必要的依赖引入到模块中

plugin Management 插件管理

对于插件而言,如果在每个子模块中分别声明插件版本及插件配置,会显得十分繁琐。为此Maven针对插件的管理提供了pluginManagement元素,其和dependencyManagement元素基本原理一致,在pluginManagement元素声明的插件及插件配置不会发生实际效果,只有当继承该POM的子模块声明了相应插件才会被实际引入,同时默认使用pluginManagement中该插件的版本及插件配置。如果某个子模块需要不同的版本及配置,则可在该子模块中显式指定以覆盖父模块POM的pluginManagement配置

下图实例中,在父POM的pluginManagement元素中声明maven-source-plugin插件并配置对模块源码进行打包的任务,子模块manager-service需要对其源码进行打包,则直接在其POM中声明该插件即可

figure 8.jpeg

聚合与继承的关系

从上文我们知道,Maven的聚合和继承是两个完全不同的概念。前者是为了快速方便地构建项目中的多个模块;后者则是为了消除POM中的重复配置。对于聚合模块来说,它知道有哪些模块被聚合,但那些被聚合的模块则不知道这个聚合模块的存在;对于父模块来说,它不知道有哪些子模块继承它,但那些子模块则必须知道其继承是哪个模块

当然,从形式上来说,聚合模块、父模块也是有一些共同点的,首先,这两种模块POM的打包类型(packaging)均为pom;其次,这种模块中除了POM文件外,均不含项目任何的实际源码。鉴于此,项目开发中,为了方便起见,一个模块是父模块的同时也可是聚合模块。但是作为开发人员,我们应该清楚这两者之间其实并无任何实际内在联系

import 依赖范围

我们在一个新的项目/模块中,如果期望复用其他POM中dependencyManagement元素的配置,我们可以通过继承或拷贝该配置实现,实际上,还有第三种办法,通过 import 将目标POM的dependencyManagement配置导入、合并到当前POM的dependencyManagement元素中,因为maven只支持单继承,如果当前项目已经继承了一个父模块,此时即可通过import导入的方式来复用其他POM中的dependencyManagement配置。需要注意的是,由于import依赖范围的特殊性,其一般指向的是打包类型为pom的模块。故其type只能为pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencyManagement>
<dependencies>
...
<dependency>
<groupId>com.tony</groupId>
<artifactId>json</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
...
</dependencies>
</dependencyManagement>

Reactor 反应堆

概述

在一个多模块的Maven项目中,反应堆Reactor是指所有模块组成的一个构建结构。对于单模块的项目来说,反应堆就是该模块本身;而对于多模块的项目来说,反应堆可以反映各模块之间的继承与依赖关系,从而能够计算出合理的模块构建顺序。下图是本文实例的组织架构图与依赖关系图

figure 9.jpeg

可以看到该项目是一个基于多模块的Maven项目,其中SpringBoot2模块为父模块(同时也是聚合模块),其下有2个模块,分别是api模块(项目启动入口)和manager模块;而对于manager模块而言,其一方面是SpringBoot2的子模块,另一方面又是 manager-mapper、manager-service、manager-controller、manager-pojo 四个模块的父模块、聚合模块。该项目中模块的依赖关系如上图左侧所示

figure 10.jpeg

当我们构建该项目时,从命令行的结果输出可以看到,其首先会根据模块的相互之间的关系计算出项目的Reactor Build Order反应堆构建顺序,然后按反应堆的构建顺序依次构建相关模块。观察反应堆可以看到,其可以保证如果模块B依赖于模块A,则模块A肯定先于模块B完成构建

裁剪反应堆

大多数时候,用户都是一次性构建整个项目,但有些时候用户可能仅仅需要构建某个模块,即用户需要裁剪反应堆来实现构建指定模块的目的。在Maven命令行中可通过相关选项参数实现反应堆的裁剪

1. 构建指定模块

1
mvn -pl, --projects <arg> # 构建指定模块,多个模块用逗号进行分隔

figure 11.jpeg

2. 同时构建所列模块的依赖模块

1
mvn -am, --also-make # 同时构建所列模块的依赖模块

figure 12.jpeg

3. 同时构建依赖于所列模块的模块

1
mvn -amd, -also-make-dependents # 同时构建依赖于所列模块的模块

figure 13.jpeg

4. 从指定模块开始构建

1
mvn -rf, -resume-from <arg> # 从指定模块开始构建

figure 14.jpeg

还可在-pl -am、-pl -amd 的基础上,进一步使用-rf参数,来对裁剪后的反应堆再次裁剪

figure 15.jpeg

参考文献

  1. Maven实战 许晓斌著
请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝