说一些自己对 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 之外都没有其他实际内容。下面分别来讲讲这两个特性,都包含哪些内容。
聚合
当一个项目存在多个模块,而且此时你需要构建时,若没有使用聚合这个特性,你只能乖乖地在每个模块下面执行打包命令,除此之外,如果有模块之间有相互依赖存在,只能按照特性顺序依次打包。
好在 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 实战》