包括项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成
maven的生命周期是抽象的,这意味着生命周期本身不做任何实际工作。每个构建步骤都可以绑定一个或者多个插件行为,而且maven为大多数构建步骤编写并绑定了默认插件,针对编译的maven-compiler-plugin,针对测试的maven-surefire-plugin,用户几乎不会觉察到插件的存在。
clean 目的是清理项目
default 目的是构建项目
site 目的是建立项目的站点
每个生命周期包含一些阶段(phase)
clean生命周期
pre-clean 执行上一次清理前需要完成的工作
clean:清理上一次构建生成的文件
post-clean 执行一些清理后需要完成的工作
default生命周期
validate
generate-sources
process-sources
generate-resources
process-resources 复制并处理资源文件,至目标目录,准备打包。
compile 编译项目的源代码。
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources 复制并处理资源文件,至目标测试目录。
test-compile 编译测试源代码。
process-test-classes
test 使用合适的单元测试框架运行测试。这些测试代码不会被打包或部署。
prepare-package
package 接受编译好的代码,打包成可发布的格式,如 JAR 。
pre-integration-test
integration-test
post-integration-test
verify
install 将包安装至本地仓库,以让其它项目依赖。
deploy 将最终的包复制到远程的仓库,以让其它开发人员与项目共享。
site生命周期
pre-site
site :生成项目的站点文档;
post-site
site-deploy :发布生成的站点文档
Maven聚合(或者称为多模块)
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元素声明父模块,parent下的子元素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>
可继承的POM元素
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>
子模块只需要配置简单的groupId和artifactId就能获得对应的依赖信息,从而引入正确的依赖。
这种依赖管理机制似乎不能减少太多的POM配置,但是其好处很大,原因在于在POM中使用dependencyManagement声明依赖能够统一规范项目中依赖的版本,当依赖的版本在父POM中声明之后,子模块在使用依赖的时候就无效声明版本,也就不会发生多个子模块使用依赖版本不一致的情况。这可以降低依赖冲突的几率。
如果在子模块不声明依赖的使用,即使该依赖已经在父POM的dependencyManagement中声明了,也不会产生任何实际的效果。
如果子模块不声明依赖的使用,即使该依赖已经在父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范围,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>
当子模块需要生成源码包的时候,只需要如下简单的配置,代码如下:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> </plugin> </plugins> </build>
子模块声明使用了maven-source-plugin插件,同时又继承了父模块的pluginManagement配置,两者基于groupId和artifactId匹配合并。
当项目中的多个模块有同样的插件配置时,应当将配置移到父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中关于项目结构的定义:
<build> <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>
这里依次定义了项目的主输出目录,主代码输出目录,最终构件的名称格式、测试代码输出格式、主源码目录,脚本源码目录、测试源码目录、主资源目录和测试资源目录。这就是Maven项目结构的约定。
接着,超级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就会报错。
裁剪反应堆
一般来说,用户会选择构建整个项目或者选择构建单个的模块,但有些时候,用户会想要仅仅构建完整反应堆中的某些个模块。换句话说,用户需要实时地剪裁反应堆。
Maven提供很多命令行选项支持裁剪反应堆,输入mvn -h可以看到这些选项:
-amd -also-make-dependents:同时构建依赖于所列模块的模块
-rf -resume-from
默认情况从account-aggregator执行mvn clean install会得到如下完整的反应堆:
[INFO]----------------------------------------------------- [INFO]Reactor Build Order: [INFO] [INFO]Account Aggregator [INFO]Account Parent [INFO]Account Email [INFO]Account Persist [INFO] [INFO]-----------------------------------------------------
可以使用-pl选项指定构建某几个模块,如运行如下命令:
$ mvn clean install -p account-email,account-persist
得到的反应堆为:
[INFO]----------------------------------------------------- [INFO]Reactor Build Order: [INFO] [INFO]Account Email [INFO]Account Persist [INFO] [INFO]-----------------------------------------------------
使用-am选项可以同时构建所列模块的依赖模块,例如:
$ mvn clean install -pl account-parent -am
由于account-email和account-persist都依赖于account-parent,因此会得到如下反应堆:
[INFO]----------------------------------------------------- [INFO]Reactor Build Order: [INFO] [INFO]Account Parent [INFO]Account Email [INFO]Account Persist [INFO] [INFO]-----------------------------------------------------
使用-rf选项可以在完整的反应堆构建顺序基础上指定从哪个模块开始构建。例如:
$mvn clean install -rf account-email
完整的反应堆构建顺序中,account-email位于第三,它之后只有account-persist因此会得到如下的剪裁反应堆:
[INFO]----------------------------------------------------- [INFO]Reactor Build Order: [INFO] [INFO]Account Email [INFO]Account Persist [INFO] [INFO]-----------------------------------------------------
最后在-pl -am或者-pl -amd的基础上,还能应用-rf参数,以对裁剪后的反应堆再次剪裁,例如:
$ mvn clean install -pl account-parent -amd -rf account-email
该命令中的-pl和-amd参数会裁剪出一个account-parent、account-email和account-persist的反应堆,在此基础上,-rf参数指定从account-email参数构建。因此会得到如下的反应堆:
[INFO]----------------------------------------------------- [INFO]Reactor Build Order: [INFO] [INFO]Account Email [INFO]Account Persist [INFO] [INFO]-----------------------------------------------------
在开发过程中,灵活应用上述四个参数,可以帮助我们跳过无须构建的模块,从而加速构建。在项目庞大、规模特别多的时候,这种效果就会异常明显。