Maven 聚合(或者称为多模块),是为了能够使用一条命令就构建多个模块,例如已经有两个模块,分别为 account-email,account-persist,我们需要创建一个额外的模块(假设名字为 account-aggregator,然后通过该模块,来构建整个项目的所有模块,accout-aggregator 本身作为一个 Maven 项目,它必须有自己的 POM,不过作为一个聚合项目,其 POM 又有特殊的地方,看下面的配置:
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifact>account-aggregator</artifact>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Aggregator</name>
<modules>
<module>account-email</module>
<module>account-persist</module>
</modules>
</project>
特殊的地方就是 packaging,其值为 pom,如果没有声明的话,默认为 jar,对于聚合模块来说,其打包方式必须为 pom,否则无法构建。
modules 里的每一个 module 都可以用来指定一个被聚合模块,这里每个 module 的值都是一个当前 pom 的相对位置,本例中 account-email、account-persist 位于 account-aggregator 目录下,当三个项目同级的时候,上面的两个 module 应该分别为../account-email 和../account-persist
在构建多个模块的时候,往往会多有模块有相同的 groupId、version,或者有相同的依赖,例如:spring-core、spring-beans、spring-context 和 junit 等,或者有相同的组件配置,例如:maven-compiler-plugin 和 maven-resources-plugin 配置,在 Maven 中也有类似 Java 的继承机制,那就是 POM 的继承。
继承 POM 的用法:
面向对象设计中,程序员可以通过一种类的父子结构,在父类中声明一些字段和方法供子类继承,这样可以做到“一处声明、多处使用”,类似的我们需要创建 POM 的父子结构,然后在父 POM 中声明一些配置,供子 POM 继承。
下面声明一个父 POM,如下:
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:shemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name>
</project>
这个父 POM 中,groupId 和 version 和其它模块一样,它的 packaging 为 pom,这一点和聚合模块一样,作为父模块的 POM,其打包类型也必须为 pom,由于父模块只是为了帮助消除配置的重复,因此它本身不包含除 POM 之外的项目文件,也就不需要 src/main/java 之类的文件夹了。
有了父模块,就需要其它模块来继承它。首先将 account-email 的 POM 修改如下:
account-email 继承 account-parent 的 POM
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:shemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.juvenxu.mvnbook.account<groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath>
</parent/>
<artifactId>account-email</artifactId>
<name>Account Email</name>
<dependencies>
....
</dependencies>
<build>
<plugins>
....
</plugins>
</build>
</project>
上面 POM 中使用 parent 元素声明父模块,paren 下的子元素 groupId、artifactId 和 version 指定了父模块的坐标,这三个元素是必须的。元素 relativePath 表示了父模块 POM 的相对位置。当项目构建时,Maven 会首先根据 relativePath 检查父 POM,如果找不到,再从本地仓库查找。relativePath 的默认值是../pom.xml,Maven 默认父 POM 在上一层目录下。
上面 POM 没有为 account-email 声明 groupId,version,不过并不代表 account-email 没有 groupId 和 version,实际上,这个子模块隐式的从父模块继承了这两个元素,这也就消除了不必要的配置。上例中,父子模块使用了相同的 groupId 和 version,如果遇到子模块需要使用和父模块不一样的 groupId 或者 version 的情况,可以在子模块中显式声明。对于 artifactId 元素来说,子模块更应该显式声明,因为如果完全继承 groupId、artifactId、version,会造成坐标冲突;另一方面,即使使用不同的 groupId 或 version,同样的 artifactId 容易造成混淆。
account-persist 继承 account-parent 的 POM
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance
xsi:shemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.juvenxu.mvnbook.account<groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath>
</parent>
<artifactId>account-persist</artifactId>
<name>Account Persist</name>
<dependencies>
....
</dependencies>
<build>
<testResources>
<testResource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
<plugins>
....
</plugins>
</build>
</project>
最后,同样需要把 account-parent 加入到聚合模块 accountp-aggregator 中,代码如下:
将 account-parent 加入到聚合模块
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifact>account-aggregator</artifact>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Aggregator</name>
<modules>
<module>account-email</module>
<module>account-persist</module>
<module>account-parent</module>
</modules>
</project>
groupId:项目组ID,项目坐标的核心元素
version:项目版本,项目坐标的核心元素
description:项目的描述信息
organnization:项目的组织信息
inceptionYear:项目的创始年份
url:项目的URL地址
developers:项目的开发者信息
contributors:项目的贡献者信息
distributionManagement:项目的部署配置
issueManagement:项目的缺陷跟踪系统信息
ciManagement:项目的集成信息
scm:项目的版本控制系统信息
mailingLists:项目的邮件列表信息
properties:自定义的Maven属性
dependencies:项目的依赖配置
dependencyManagement:项目的依赖管理配置
repositories:项目的仓库配置
build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等
reporting:包括项目的报告输出目录配置,报告插件配置等。
当多有模块中有相同的依赖时,我们可以将这些依赖提取出来,同时在父 POM 中声明,这样就可以简化子模块的配置了,但是这样还是存在问题,当想项目中加入一些,不需要这么多依赖的模块,如果让这个模块也依赖那些不需要的依赖,显然不合理。
Maven 提供的 dependentcyManagement 元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活度。
在 dependentcyManagement 元素下的依赖声明不会引入实际的依赖,不过他能够约束 denpendencies 下的依赖使用。对上面的 accoun-parent 进行改进,代码如下:
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:shemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name
<properties>
<springframework.version>2.5.6</springframework.version>
<junit.version>4.7</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${springframework.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
上面使用了 dependencyManagement 声明的依赖既不会给 account-parent 引入依赖,也不会给它的子模块引入依赖,不过这段配置是会被继承的。
修改 account-email 的 POM 如下:
继承 dependencyManagement 的 account-email POM
<properties>
<javax.mail.version>1.4.1</javax.mail.version>
<greenmail.version>1.3.1b</greenmail.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>${javax.mail.version}</version>
</dependency>
<dependency>
<groupId>javax.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>${greenmail.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
子模块只需要配置简单的 groupId 和 artifactId 就能获得对应的依赖信息,从而引入正确的依赖。
这种依赖管理机制似乎不能减少太多的 POM 配置,但是其好处很大,原因在于在 POM 中使用 dependencyManagement 声明依赖能够统一规范项目中依赖的版本,当依赖的版本在父 POM 中声明之后,子模块在使用依赖的时候就无效声明版本,也就不会发生多个子模块使用依赖版本不一致的情况。这可以降低依赖冲突的几率。
如果子模块不声明依赖的使用,即使该依赖已经在父 POM 的 dependencyManagement 中声明了,也不会产生任何实际的效果,如 account-persist 的 POM。
<properties>
<dom4j.version>1.6.1</dom4j.version>
</properties>
<dependencies>
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>dom4j.version</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
这里没有声明 spring-context-support,那么该依赖就不会被引入。这正是 dependencyManagement 的灵活性所在.
import 依赖范围只有在 dependencyManagement 下才有效果,使用该范围的依赖通常指向一个 POM,作用是将目标中的 dependencyManagement 配置导入到当前 POM 的 dependencyManagement 中配置。
除了复制配置或者继承这两种方式之外,还可以使用 import 范围依赖将这一配置导入。 使用 import 范围依赖导入依赖管理配置
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook.account<groupId>
<artifactId>account-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
注意,上面代码中依赖的 type 为 pom,import 依赖范围由于其特殊性,一般都指向打包类型为 pom 的模块。如果有多个项目,他们使用的依赖版本都是一致的,则可以定义一个 dependencyManagement 专门管理依赖的 POM,然后在各个项目中导入这些依赖管理配置。
Maven 提供了 dependencyManagement 元素帮忙管理依赖,类似地,Maven 也提供了 pluginManagement 元素帮忙管理插件。该元素中配置的依赖不会造成实际的插件调用行为,当 POM 中配置了真正的 plugin 元素,并且其 groupId 和 artifactId 与 pluginManagement 中配置的插件匹配时,pluginManagement 的配置才会影响实际的插件行为。
前面有如何通过组件去将 jar-no-fork 目标绑定到 verity 生命周期阶段,以生成项目源码包。如果一个项目中有很多子模块,并且需要这些模块的源码包,那么很显然,为所有模块重复类似的插件配置不是最好的办法,这时最好的办法是在父 POM 中使用 pluginManagement 配置插件,代码如下:
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
当子模块需要生成源码包的时候,只需要如下简单的配置,代码如下: 子模块声明使用了 maven-source-plugin 插件,同时又继承了父模块的 pluginManagement 配置,两者基于 groupId 和 artifactId 匹配合并。
有了 pluginManagement 元素,accout-email 和 accout-persist 的 POM 也能得以简化了他们都配置了 maven-compiler-plugin 和 maven-resources-plugin。可以将这两个插件的配置移到 account-parent 的 pluginManagement 元素中,代码如下:
<build>
<pluginMangement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifact>maven-resources-plugin</artifact>
<configuration>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginMangement>
</build>
accout-email 和 account-persist 可以完全地移除关于 maven-compiler-plugin 和 maven-resources-plugin 的配置,但他们仍然能享受这这个插件的服务,前一插件开启了 jdk1.8 编译的支持,后一插件也会使用 UTF-8 编码处理资源文件。这背后涉及了很多 Maven 机制,首先,内置的插件绑定关系将两个插件绑定到了 account-email 和 account-persist 生命周期上,其次,超级 POM 为这两个插件申明了版本;最后,account-parent 中的 pluginManagement 对这两个插件的行为进行了配置。
当项目中的多个模块有同样的插件配置时,应当将配置移到父 POM 的 pluginMangement 元素中。即使各个模块对于同一插件的具体配置不尽相同,也应当使用父 POM 的 pluginManagement 元素同意声明插件的版本。甚至可以要求将所有用到的插件的版本在父 POM 的 pluginManagement 中声明,子模块使用插件时不配置版本信息,这么做可以统一项目的插件版本,避免潜在的插件不一致或者不稳定的问题,也更易于维护。
多模块中的聚合与继承其实是两个概念,其目的是完全不同的,前者主要是为了方便快速构建项目,后者主要是为了消除重复配置。
对于聚合模块来说,它知道有哪些被聚合的模块,但那些被聚合的子模块不知道这个聚合模块的存在。
对于继承关系的父 POM 来说,它不知道哪些子模块继承于它,但那些子模块都必须知道自己的父 POM 是什么。
在现有的实际项目中,往往会发现一个 POM 即是聚合 POM,又是父 POM,这么做主要是为了方便。一般来说,融合使用聚合与继承也没什么问题,例如可以将 account-aggretor 和 account-parent 其 POM 如下:
<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
http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name>
<modules>
<module>account-persist</module>
</modules>
<properties>
<springframework.version>2.5.6</springframework.version>
<junit.version>4.7</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>junit<groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resource-plugin<artifactId>
<configuration>
<endcoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
可以看到 POM 的打包方式为 pom,它包含了一个 modules 元素,表示用来聚合 account-persist 和 account-email 两个模块,它还包含了 properties、dependencyManagement 和 pluginManagement 元素供子模块继承。
Maven 默认的源码目录是:src/main/java 但是用户也可以自己指定源码目录,如下:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook<groupId>
<artifactId>my-project</artifactId>
<version>1.0</version>
<build>
<sourceDirectory>src/java</sourceDirectory>
</build>
</project>
上面配置就将源码目录改为 src/java 而不是默认的 src/main/java
任何一个 Maven 项目都隐式的继承自该 POM,这有点类似任何一个 Java 类都隐式的继承于 Object 类,因此,大量超级 POM 的配置都会被所有 Maven 项目继承,这些配置也就成为了 Maven 所提倡的约定。
对于 Maven3,超级 POM 在文件 MAVEN_HOME/lib/maven-model-builder-x.x.x.jar 中的 org/apache/maven/model/pom-4.0.0.xml 路径下。对于 Maven2,超级 POM 在文件 MAVEN_HOME/lib/maven-x.x.x-uber.jar 中的 org/apache/maven/project/pom-4.0.0.xml 目录下。
超级 POM 的内容在 Maven2 和 Maven3 中基本一致,分段看一下,见代码:
<repositories>
<repository>
<id>central</id>
<name>Maven Repository Switchboard</name>
<url>http://repo1.maven.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Maven Plugin Repository</name>
<url>http://repo1.maven.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>
超级 POM 定义了仓库及插件仓库,两者的地址都是为中央仓库http://repo1.maven.org/maven2,并且都关闭了SNAPSHOT的支持,这也就解释了为什么Maven默认就可以按需要从中央仓库下载构件。
超级 POM 中关于项目结构的定义:
这就是 Maven 项目结构的约定:
<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>src/main/script</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 为核心差劲设定版本,代码如下:
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.2-bete-4</version>
</plugin>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.3</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.2</version>
</plugin>
........
</plugins>
</pluginManagement>
</build>
在一个多模块的 Maven 项目中,反应堆(Reactor)是指所有模块组成的一个构建结构。对于单模块的项目,反应堆就是该模块本身,对于多模块来说,反应堆就包含了各模块之间继承与依赖的关系,从而能够自动计算出合理的模块构建顺序。
例如:
<modules>
<module>account-email</module>
<module>account-persist</module>
<module>account-parent</module>
</modules>
以上,构建顺序不一定是顺序去读取 POM 的顺序,当一个模块依赖于另外一个模块,Maven 会先去构建被依赖模块,并且 Maven 中不允许出现循环依赖的情况,就是,当出现模块 A 依赖于 B,而 B 又依赖于 A 的情况时,Maven 就会报错。