1

(七)聚合与继承

软件设计人员往往会采用各种方式对软件划分模块,以得到更清晰的设计及更高的重用性。
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文件即可,因为聚合模块仅仅是帮助聚合其他模块构建工具,它本身并无实质的内容。
如下图
clipboard.png

执行聚合项目声明周期时,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个参数,可以帮助我们跳过无须构建的模块,加速构建。当项目庞大、模块特别多时,这种效果就会异常明显。


roylion
204 声望25 粉丝

读书破万卷