背景
在Maven世界里,每一个构件(artifact)都有一个唯一的坐标(Coordinate)。坐标是用一系列元素来定义的:
groupId artifactId version packaging classifier
其中, groupId 、 artifactId 、 version 是必需的,packaging是可选的(默认值为 jar ),而classifier是不能直接定义的。
问题
在pom文件中,我们可以定义依赖关系,例如:
............
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
............
<dependencies>
............
被依赖的artifact(本例中是 spring-boot-starter-web ),显然也需要 groupId 、 artifactId 、 version 来唯一标识,然而,这里只有 groupId 和 artifactId ,并没有 version 。这是怎么回事呢?
带着这个问题,我们来看一下Maven的依赖(dependency)。
聚合和继承
一个项目(project)的开发,通常采用“模块化开发”的方式,项目是由多个模块(module)所组成。模块秉承“高内聚,低耦合”的原则,例如开发团队分为多个小组,每组只专注于各自所负责的模块的开发和调试。但项目作为一个整体,需要在总体上进行构建。
Maven提供了 聚合 和 继承 这两个概念:
- 聚合:把各个模块聚合在一起构建;
- 继承:抽取各个模块相同的依赖和插件等配置,以简化配置,方便维护;
聚合
聚合比较简单,就是把多个模块在一起构建。例如,我们有 module1 和 module2 两个模块。为了把它们聚合在一起,我们再创建一个额外的 aggregator 模块。该模块本身作为一个Maven项目,有自己的pom文件,但是它不涉及项目的具体业务逻辑,所以没有src等目录。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.demo0402</groupId>
<artifactId>aggregator</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Aggregator</name>
<modules>
<module>module1</module>
<module>module2</module>
</modules>
</project>
该pom文件的核心配置是:
packaging :必须是 pom ;modules :要聚合的模块。注意此处的每一个值都是针对当前pom文件的相对路径。通常可以令目录名与其artifact名一致,以便于理解和快速定位,不过也不强制,反正记住这里要写目录名;
一般我们把聚合模块作为module1、module2等模块的父一级目录,即:
? test0402 tree
.
└── aggregator
├── module1
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ └── java
├── module2
│ ├── pom.xml
│ ├── src
│ │ ├── main
│ │ │ ├── java
│ │ │ └── resources
│ │ └── test
│ │ └── java
└── pom.xml
在做build的时候,可以看到构建的细节,例如:
? aggregator mvn clean install
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] module1 [jar]
[INFO] module2 [jar]
[INFO] Aggregator [pom]
............
[INFO] Reactor Summary for Aggregator 0.0.1-SNAPSHOT:
[INFO]
[INFO] module1 ............................................ SUCCESS [ 11.691 s]
[INFO] module2 ............................................ SUCCESS [ 4.185 s]
[INFO] Aggregator ......................................... SUCCESS [ 0.060 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 16.123 s
[INFO] Finished at: 2022-04-03T17:48:02+08:00
[INFO] ------------------------------------------------------------------------
注意上面显示的是各个模块的 name ,而不是 artifactId 。
继承
module1 和 module2 模块有很多相同的配置,比如 groupId 和 version ,此外还有 spring-boot-starter-web 等三方依赖包也一样。重复的配置显然增加了维护成本,并带来潜在的不一致风险。
Maven提供了继承机制,来抽取重复的配置。这有点类似于面向对象的父子类关系,在父POM中定义一些配置以供子POM来继承。
“父POM”与“聚合POM”有些相似之处:
packaging 必须为 pom ;- 不需要src目录;
在 aggregator 目录下创建 parent 目录,然后在 parent 目录里创建 pom.xml 文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example.demo0402</groupId>
<artifactId>parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Parent</name>
</project>
有了父模块, module1 和 module2 还必须显式的声明继承关系。
以module1为例,在其pom.xml文件中添加如下内容:
<parent>
<groupId>com.example.demo0402</groupId>
<artifactId>parent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../parent/pom.xml</relativePath>
</parent>
module1 可以删除自己的 groupId 和 versio 设置,默认会从父模块继承。这样,各个模块保持相同的groupId和version。
注: relativePath 的默认值是 ../pom.xml ,即上一级目录
接下来,我们来把dependency抽取到父模块。
若直接把module1和module2公共的dependency配置直接移到父模块,固然OK,不过这种做法存在一个问题,如果以后再有module3,而module3并不需要这些dependency,这就难办了。
所以Maven提供了 dependencyManagement 元素。在父模块中通过 dependencyManagement 元素声明dependency,而在子模块中通过 dependency 元素引用所需的dependency。
在父模块POM中添加如下内容:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.6</version>
</dependency>
</dependencies>
</dependencyManagement>
在子模块POM的 dependencies 中添加如下内容:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
可见,子模块的dependency只需指明 groupId 和 artifactId ,而无需指定 version 。这是因为完整的dependency信息是在父模块的 dependencyManagement 定义的,所以子模块只需通过 groupId 和 artifactId 来引用即可。
这种做法的好处是:
- 统一性:所有dependency都由父模块统一定义,确保子模块使用的dependency版本一致;
- 灵活性:子模块只需引用自己所需的dependency;
有一种特殊的依赖范围,叫做 import 。这种依赖通常指向一个POM,作用是将其 dependencyManagement 配置导入到当前的POM中。例如:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example.demo0402</groupId>
<artifactId>xxx</artifactId>
<version>0.0.1-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.example.demo0402</groupId>
<artifactId>yyy</artifactId>
<version>0.0.1-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
............
</dependencies>
</dependencyManagement>
常见做法是把依赖根据类型不同,生成多个POM(如上例中的 xxx 、 yyy 等),然后在父模块中将其导入,以便“分而治之”。
聚合与继承的关系
共同点
packaging 必须是 pom ;- 除了POM外并无src等实际内容;
区别
- 聚合:为了方便快速构建整个项目。聚合POM知道被聚合的POM,而被聚合的POM不知道聚合POM;
- 继承:为了消除重复配置。父POM不知道子POM,子POM知道父POM;
集成
为了方便,通常可以把聚合POM和父POM合二为一,只用一个POM。具体例子参见代码。
代码
https://github.com/dukeding/test0402
|