再解 Maven 的 '聚合' 与 '继承'

926 阅读6分钟

说一些自己对 Maven 聚合 和 继承特性的理解,之前写过的 Maven 小结 对于 聚合 和 继承特性这块谈的笼统,这篇文章把它拎出来单独说说

特性关系

首先,需要指出的是多模块项目下的 聚合继承 是两个概念,其目的是完全不同的聚合主要是为了方便快速的构建项目,继承 主要是为了消除重复配置。也就是说项目可以分别单独使用聚合继承 这两个特性,也可以同时使用。

对于聚合 pom 来说,它需要知道哪些模块被聚合,但那些被聚合的模块的并不知道此聚合 pom 的存在。以下是一个聚合 pom 示例:

<project xmlns="...">
    <modelVersion>4.0.0</modelVersion>
    <groupId>xxx</groupId>
    <artifactId>xxx</artifactId>
    <packaging>pom</packaging>
    <name>xxx</name>
    <version>xxx</version>

    <modules>
        <module>a-module</module>
        <module>b-module</module>
        <module>c-module</module>
    </modules>
</project>

对于继承 pom 来说,它不知道哪些子模块继承自它,但那些子模块必须知道自己的父 pom 的存在。以下是一个子模块 pom 示例:

<project xmlns="...">
    <parent>
        <groupId>com.fingard.rh.rhf</groupId>
        <artifactId>parent</artifactId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>

    <artifactId>xxx</artifactId>
    <name>xxx</name>
    <packaging>war</packaging>
</project>

除了这些不同点,两者也有一些共同点,如:其打包方式(packing)都必须是 pom,同时工程文件夹下除了 pom.xml 之外都没有其他实际内容。下面分别来讲讲这两个特性,都包含哪些内容。

聚合继承1

聚合

当一个项目存在多个模块,而且此时你需要构建时,若没有使用聚合这个特性,你只能乖乖地在每个模块下面执行打包命令,除此之外,如果有模块之间有相互依赖存在,只能按照特性顺序依次打包。

好在 Maven 提供了 聚合 特性,它不仅能提供一键构建整个项目,还能在构建时自动计算模块之间的依赖关系(先构建哪个后构建哪个,反应堆构建顺序)。使用也很简单,只需要再单独创建一个 聚合 模块,通过构建该模块来构建整个项目的所有模块。

<project xmlns="...">
    <modelVersion>4.0.0</modelVersion>
    <groupId>xxx</groupId>
    <artifactId>xxx</artifactId>
    <packaging>pom</packaging>
    <name>xxx</name>
    <version>xxx</version>

    <modules>
        <module>a-module</module>
        <module>b-module</module>
    </modules>
</project>

增加 modules 列表元素,值分别是项目中每个 module 的与当前 pom 的相对路径(注意:值是 name 而不是 artifactId)。上面的示例,是将 聚合模块 与 其他模块的目录结构形成父子关系。

根目录:聚合模块文件夹
|---a-module
|---b-module
|聚合模块 pom.xml

当然也可以和其他模块平行,如下

根目录:工程名
|---聚合模块
|---a-module
|---b-module

聚合模块的 pom 也需要做相应的修改

<modules>
    <module>../a-module</module>
    <module>../b-module</module>
</modules>

为了直观和方便构建,通常将聚合模块放在目录的最顶层,其他的模块则作为聚合模块的子目录存在。

继承

假如有如下场景 a-module、b-module 两模块化不相互依赖且同时依赖了 spring-core,而你此时必须将依赖坐标分别添加各自的 pom 中,日后若想统一升级该依赖的版本号,就必须改动两个项目,如果其中之一忘记升级,还会导致意料之外的 bug。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.1.5.RELEASE</version>
</dependency>

为了解决这个问题,消除重复的配置,可以引入继承特性。跟 聚合 一样,需要创建一个单独的 maven 模块来充当整个项目的父模块,将其他模块置于其目录下。

根目录:父模块文件夹
|---a-module
|---b-module
|父模块 pom.xml

同时,父模块的打包方式与聚合一样也是 pom,且必须为此方式。

<project xmlns="...">
    <modelVersion>4.0.0</modelVersion>
    <groupId>xxx</groupId>
    <artifactId>xxx</artifactId>
    <version>xxx</version>
    <packaging>pom</packaging>
    <name>xxx-parent</name>

    <dependencies>
    ....
    </dependencies>

    <buid>
        <plugins>
        ...
        </plugins>
    </build>
</project>

现在来看看子模块(a-module)如何继承该父模块

<project xmlns="...">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>xxx</groupId>
        <artifactId>xxx</artifactId>
        <version>xxx</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <artifactId>a-module</artifactId>
    <name>a-module</name>
</project>

需要注意的是,parent>relativePath 标签值默认为 ../pom.xml,也就是子项目位于父项目的文件夹下,如果按照上述所说的方式摆放的文件结构,那么这个标签可以省略。

继承了父 pom 的子模块可以省略 <version><groupId> 标签,若省略则用父 pom 中定义的,但是 <artifactId> 标签不可省略,如果用继承的,则会导致坐标混乱。除了上述所说可继承元素,以下所列元素都是可以继承的

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

利用这些可继承的元素,可以在父模块做一些统一的管理工作,如依赖管理、插件管理。例如解决一开始 spring-core 包的统一依赖问题。

在父 pom 的 <dependencyManagement> 标签里面声明需要统一管理的三方依赖

<!-- 父 pom -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
    </dependencies>
</dependencyManagement>

此时继承了该 pom 的子模块并不会依赖 spring-core,还需要在子 pom 再声明该依赖(相当于 Java 中的父类方法覆写)

<project xmlns="...">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>xxx</groupId>
        ...
    </parent>

    <artifactId>a-module</artifactId>
    <name>a-module</name>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
    </dependencies>
</project>

可以观察到,虽然在子 pom 中再次声明了该依赖,但与往常不同的是并没有加 <version> 版本号,此时代表沿用父 pom 中声明的版本号。以此类推,其他的继承父 pom 的模块也可以使用该种方式进行依赖声明。如此一来,当需要对共同的依赖进行升级时,只需要改动父 pom 中版本号就能完成所有子模块的升级。

但你可能又会问了,对这些共同依赖,我要是不想再复写两遍声明该怎么办?也好办!观察能支持继承特性的标签里边还有 <dependencies>。也就是说只需要将共同都有的依赖置于 <dependencies> 标签中,所有继承自该 pom 的子模块,都将自动继承该依赖,不需要子 pom 中重复声明。

<!-- 父 pom -->
<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>5.1.5.RELEASE</version>
    </dependency>
</dependencies>

除了上面所说两标签常用,<build> 标签也能继承,下面是一个示例,声明了一个资源打包插件,配置了一些过滤的参数,当子模块继承了该 pom,也就相当于配置了此插件。

<!-- 父 pom -->
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>2.4</version>
            <configuration>
                <encoding>UTF-8</encoding>
                <!--
                过滤后缀为 pem、pfx 的证书文件
                maven 打包后,会编译证书文件,导致无法加载
                    -->
                <nonFilteredFileExtensions>
                    <nonFilteredFileExtension>key</nonFilteredFileExtension>
                    <nonFilteredFileExtension>cer</nonFilteredFileExtension>
                    <nonFilteredFileExtension>pem</nonFilteredFileExtension>
                    <nonFilteredFileExtension>pfx</nonFilteredFileExtension>
                    <nonFilteredFileExtension>p12</nonFilteredFileExtension>
                    <nonFilteredFileExtension>truststore</nonFilteredFileExtension>
                </nonFilteredFileExtensions>
            </configuration>
        </plugin>
    </plugins>
</build>

小结

在企业开发中,一般将两特性在一个 pom 中应用,根目录的 pom 既是聚合模块又是父模块。

作为聚合模块,在打包时,只需要在根目录的 pom 中执行一次 maven 命令就可构建整个项目,同时项目结构又能保持直观。

作为父模块,对于公用的包例如 JUint、apache Common 这种项目,直接在父 pom <dependencies> 进行声明让所有子模块都依赖。同时,将所有的包都在 <dependencyManagement> 中进行声明,当子模块需要依赖时,覆盖即可。同时,对于一些公用的插件也可以且应该在父 pom 中进行声明。

总的来说,两特性不难理解,跟平常写代码用到的设计模式有点像,聚合应用到了统一调度思想,继承应用了复用思想。

参考

  • 《Maven 实战》