(七)聚合与继承
软件设计人员往往会采用各种方式对软件划分模块,以得到更清晰的设计及更高的重用性。
Maven聚合特性,将项目的各个模块聚合在一起构建。
Maven继承特性,抽取各模块相同的依赖和插件等配置。
聚合
聚合项目,顾名思义,就是将多个项目聚合在一起。
通常情况下,聚合项目的目录结构如下
|-parent <!-- 父工程是一个Maven项目 --> |-parent_POM <!-- 父工程拥有自己的POM文件,packaging元素值为pom,在modules中添加module元素实现项目聚合 --> |-sub1 <!-- 子工程通常位于父工程目录下,该目录通常与子工程artifactId相同,但不是必须 --> |-sub1_POM <!-- 父工程POM中module的值为子工程POM所在目录(相对目录,相对于父工程目录) --> |-sub2 <!-- --> |-sub2_POM <!-- -->
通常情况下,子工程位于父工程目录下,且子工程目录名与其artifactId相同,但实际情况由module的值所决定,即module值的路径能定位到子工程POM文件即可,对目录命名与布局没有绝对要求。
同时父工程只需pom文件即可,因为聚合模块仅仅是帮助聚合其他模块构建工具,它本身并无实质的内容。
如下图
执行聚合项目声明周期时,Maveng会首先解析模块的POM、分析要构建的模块、并计算出一个反应堆构建顺序(Reactor Build Order),然后根据这个顺序依次构建各个模块。反应堆是所有模块组成的一个构建结构。
继承
Maven的继承特性可以抽取重复的配置。在父POM中声明一些配置供子类POM继承,实现"一处声明,多处使用"。
同聚合模块一样,继承父工程是一个maven项目,拥有自己的POM文件,packaging类型为pom。由于父工程只是帮助消除重复配置,因此其本身不包含除POM之外的项目文件。
1)继承声明
在父工程POM文件中声明复用依赖或插件等配置,安装到本地仓库即可由其他子项目继续。
在子项目POM文件中声明父工程。
同时子项目可省略groupId和version,因为子项目POM会隐式继承父项目POM的groupId和version。
... <parent> <groupId>...</groupId> <!-- 必须,声明父工程的groupId --> <artifactId>...</artifactId> <!-- 必须,声明父工程的artifactId --> <version>...</version> <!-- 必须,声明父工程的version --> <relativePath>...</relativePath> <!-- 可省略,默认为../pom.xml,声明父工程的POM文件所在路径 --> </parent> <artifactId>...<artifactId> <!-- 省略groupId和version,隐式继承父POM的groupId和version --> <name>...</name> ...
子项目构建时,Maven会根据relativePath检查父POM,如果找不到,再从仓库查找。
正确设置relativePath非常重要,如果团队新成员从源码库检出一个包含父子模块关系的Maven项目,由于只关心子项目,当其直接到子项目下执行构建,由于本地仓库没有安装父项目,将直接导致构建失败。如果Maven能够根据relativePath找到父POM,它就不需要再去检查本地仓库。
2)可继承的POM元素
groupId | 项目组ID,项目坐标的核心元素 |
version | 项目版本,项目坐标的核心元素 |
description | 项目的描述信息 |
organization | 项目的组织信息 |
inceptionYear | 项目的创始年份 |
url | 项目的URL地址 |
developers | 项目的开发者信息 |
contributors | 项目的贡献者信息 |
distributionManagement | 项目的部署配置 |
issueManagement | 项目的缺陷跟踪系统信息 |
ciMananagement | 项目的持续集成系统信息 |
scm | 项目的版本控制系统信息 |
mailingLists | 项目的邮件列表信息 |
properties | 自定义的Maven属性 |
dependencies | 项目的依赖配置 |
dependencyManagement | 项目的依赖管理配置 |
repositories | 项目的仓库配置 |
build | 包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等 |
reporting | 包括项目的报告输出目录配置、报告插件配置等 |
3)依赖管理
dependencyManagement 依赖管理配置,在该元素中声明的依赖不会被引入,而是起到约束并简化子项目依赖的作用,子项目会继承其声明的依赖配置,如version,scope。springboot项目的父工程应该是配置了该依赖管理配置,实现项目依赖无需填写版本号。
import依赖范围,只在dependencyManagement元素中生效,可以导入其他项目的dependencyManagement配置
<dependencyManagement> <dependencies> <dependency> <groupId>...</groupId> <artifactId>...</artifactId> <version>...</version> <type>pom</type> <scope>import</scope> </dependency> ... </dependencies> </dependencyManagement>
4)插件管理
类似于dependencyManagement元素,pluginManagement元素用于管理插件,在该元素中配置的依赖不会造成实际的插件调用方式,可以理解为dependencyManagement 声明了插件依赖的配置,当其子类声明该插件依赖时,可复用该配置,起到约束和简化作用。
Ps:什么情况下可以用到依赖管理和插件管理,当一些依赖或插件依赖只有部分子模块需要时,应该由子模块自身去声明引入依赖并配置,而这些配置都是重复的,通过依赖管理和插件管理可以实现配置的复用,并且不会给其他模块引入不必要的依赖。
聚合和继承的关系
聚合 | 概念 | 方便快速构建项目 |
---|---|---|
不同点 | 对于聚合模块,它需要知道哪些被聚合的模块,但被聚合的模块不知道这个聚合模块的存在 | |
继承 | 概念 | 消除重复配置 |
不同点 | 对于继承关系的父POM,它不知道哪些模块继承于它,但那些子模块必须知道自己的父POM | |
相同点 | 聚合模块和继承关系中的父POM的packaging都必须是pom。同时,它们除了POM文件外都没有实际内容 |
预定优于配置
标准很重要,Web应用开发基于HTTP协议、Java屏蔽大部分操作系统差异,实现跨平台、所有语言都支持XML。
如果没有约定,10个项目就可能使用10种不同的项目目录结构,这意味着交流学习成本增加。
遵循Maven的约定
源码目录 | src/main/java |
编译输出目录 | target/classes |
打包方式 | jar |
包输出目录 | target |
遵循约定虽然损失一定的灵活性,无法随意安排目录结构,但能减少配置,帮助用户遵守标准。
Maven可自定义源码目录
<build> <sourceDirectory>...</sourceDirectory> <!-- 不推荐自定义源码目录,提高交流成本 --> </build>
关于超级POM
任何一个Maven项目都隐式继承了超级POM。该文件位于lib/maven-model-builder-x.x.x.jar中的org/apache/maven/model/pom-4.0.0.xml。
首先,超级POM定义了中央仓库和插件仓库,两者的地址都为中央仓库,并且都关闭了SNAPSHOT的支持。
其次,定义了项目结构
<!-- 主输出目录 --> <directory>${project.basedir}/target</directory> <!-- 主代码输出目录 --> <outputDirectory>${project.build.directory}/classes</outputDirectory> <!-- 最终构件的名称格式 --> <finalName>${project.artifactId}-${project.version}</finalName> <!-- 测试代码输出目录 --> <testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory> <!-- 主源码目录 --> <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory> <!-- 脚本源码目录 --> <scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory> <!-- 测试源码目录 --> <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory> <!-- 主资源目录 --> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> </resource> </resources> <!-- 测试资源目录 --> <testResources> <testResource> <directory>${project.basedir}/src/test/resources</directory> </testResource> </testResources>
最后,超级POM通过插件管理为核心插件设定了版本,防止由于插件版本的变化而造成构建不稳定。
反应堆
在一个多模块的Maven项目中,反应堆(Reactor)是指所有模块组成的一个构建结构。
对于单模块的项目,反应堆就是其本身,对于多模块而言,反应堆包含了各模块之间继承与依赖的关系,从而能够自动计算出合理的模块构建顺序。
1)反应堆的构建顺序
Maven按序读取POM,如果该POM没有依赖模块,则构建该模块,否则就先构建其依赖模块,如果该依赖还依赖于其他依赖,则进一步构建依赖的依赖。
模块间的依赖关系会将反应堆构成一个有向非循环图(Directed Acyclic Graph,DAG),各个模块是该图的节点,依赖关系构成了有向边。此图不允许循环,因此,当出现A依赖B,而B又依赖A时,Maven就会报错。
2)裁剪反应堆
一般来说,用户会选择构建整个项目或选择构建单个模块。
但有时,用户会想要构建完整反应堆中的部分模块,即裁剪反应堆。
Mave提供很多命令行选择支持裁剪反应堆。输入mvn -h 可以看到这些选项
-am | --also-make | 同时构建所列模块的依赖模块 |
-amd | -alse-make-dependents | 同时构建依赖于所列模块的模块 |
-pl | --projects <arg> | 构建指定模块,模块间用逗号分隔 |
-rf | -resume-from <arg> | 从指定的模块回复反应堆,在完整的反应堆顺序基础上指定从哪个模块开始构建 |
在开发过程中,灵活应用上述4个参数,可以帮助我们跳过无须构建的模块,加速构建。当项目庞大、模块特别多时,这种效果就会异常明显。