五、生命周期和插件
Maven的生命周期包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等,几乎所有的构建步骤。
Maven的生命周期是抽象的,定义了各个构建步骤的次序,但没有提供具体实现。具体的构建步骤由插件实现。通过插件机制,每个构建步骤可以绑定一个或者多个插件行为。
1、生命周期
(1)、三套生命周期
Maven拥有三套相互独立的生命周期:clean、default、site。
每个生命周期包含一些阶段(phase),这些阶段是有顺序的,后面的阶段依赖于前面的阶段。
【】clean生命周期
目的是清理项目,包含三个阶段:
** pre-clean : 执行一些清理前需要完成的工作
** clean:清理上一次构建生成的文件
** post-clean:执行一些清理后需要完成的工作
【】default生命周期
目的:定义真正构建时所需的执行步骤,其包含的阶段如下:
** validate
** initialize
** generate-sources
** process-sources : 处理项目主资源 文件,一般是对src/main/resources目录的内容进行变量替换等工作后,复制到项目输出的主classpath目录中。
** generate-resources
** process-resources
** compile :编译项目的主源码,一般是编译src/main/java目录下的Java文件至项目输出的主classpath目录中。
** process-classes
** generate-test-sources
** process-test-sources : 处理项目测试资源文件。一般来说,是对src/test/resources目录的内容进行变量替换等工作后,复制到项目输出的测试classpath目录中。
** generate-test-resources:
** prosess-test-resource:
** test-compile:编译项目的测试代码,一般来说,是编译src/test/java目录下的Java文件至项目输出的测试classpath目录中。
** process-test-classes:
** test:使用单元测试框架运行测试,测试代码不会被打包或部署
** prepare-package
** package:接受编译好的代码,打包成可发布的格式,如JAR。
** pre-integration-test
** integration-test
** post-integration-test
** verify
** install : 将包安装到Maven本地仓库,供本地其他Maven项目使用
** deploy:将最终的包复制到远程仓库,供其他开发人员和Maven项目使用。
【】site生命周期
site生命周期的目的是建立和发布项目站点,Maven能够基于POM所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:
** pre-site:执行一些在生成项目站点之前需要完成的工作。
** site:生成项目站点文档
** post-site:执行一些在生成项目站点之后需要完成的工作。
** site-deploy:将生成的项目站点发布到服务器上。
2、插件目标(Plugin Goal)
Maven的核心仅仅定义了抽象的生命周期,具体的任务是交由插件完成的,插件以独立的构件形式存在。对于插件来讲,每一插件都能完成多个功能,每个功能就是一个插件目标。
关于插件目标的通用写法是:冒号前面是插件前缀,冒号后面是该插件的目标。例如:dependency:analyze、dependency:tree和dependency:list
3、插件绑定
Maven生命周期的阶段与插件的目标相互绑定,以完成某个具体的构建任务。例如,对于项目编译这一任务,它对应了dafault生命周期的compile这一阶段,而maven-compiler-plugin这一插件的compile目标能够完成该任务,因此,将它们绑定,就能实现项目编译的目的。
(1)、内置绑定
为了能让用户几乎不用任何配置就能构建Maven项目,Maven在核心为一些主要的生命周期阶段绑定了许多插件的目标,当用户通过命令行调用生命周期阶段的时候,对应的插件目标就会执行相应的任务。
clean生命周期中的clean阶段与maven-clean-plugin:clean绑定;
site生命周期的site阶段与maven-site-plugin:site绑定;
site生命周期的site-deploy阶段与maven-site-plugin:deploy绑定;
由于项目的打包类型会影响构建的具体过程,因此default生命周期的阶段与插件目标的绑定关系由项目打包类型所决定。
基于jar打包类型的项目,其dafault生命周期的内置插件绑定关系及具体任务如下表:
注意:default生命周期还要很多其他阶段,默认它们没有绑定任何插件,因此也没有任何实际行为。
打包类型除jar之外,常见的还有war、pom、maven-plugin、ear等。
(2)、自定义绑定
除了内置绑定以外,用户还能够自动选择将某个插件目标绑定到生命周期的某个阶段。
具体做法:
在项目的POM文件的build元素下的plugins子元素中声明插件的使用,除了基本的插件坐标声明外,还有配置插件的执行配置,executions下每个execution子元素可用来配置执行一个任务。executions子元素下的id指定任务名,phrase配置绑定的生命周期阶段,goals配置指定要执行的插件目标。
注意:对于自定义绑定的插件,用户不应该使用快照版本,这样可以避免由于插件版本变化造成的构建不稳定性。
当多个插件目标绑定到同一个阶段的时候,这些插件声明的先后顺序决定了目标的执行顺序。
4、插件配置
几乎所有的Maven插件的目标都有一些可配置的参数,用户可通过命令行和POM配置等方式来配置这些参数。
(1)、命令行插件配置
在Maven命令中使用-D参数,并伴随一个参数键=参数值的形式,来配置插件目标的参数。例如跳过测试执行的参数设置:
mvn install -D maven.test.skip = true
对于一些经常变化的开关类配置参数,可以通过命令行配置。
(2)、POM中插件全局配置
在POM文件,<build>元素下的<plugins>元素下的<plugin>元素中直接配置子元素<configuration>,那么所有基于该插件目标的任务,都会使用这些配置。
对于一些项目整个过程中都不变的一些配置参数,可以使用POM插件全局配置。
(3)、POM中插件任务配置
用户还可以为某个插件任务配置特定的参数。在execution元素下配置子元素<configuration>,就是对特定任务的配置。
例如,一个插件目标可以绑定到多个生命周期阶段上,加上不同的配置,就可以让Maven在不同的生命周期阶段执行不同的任务,如:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
<executions>
<execution>
<id>ant-validate</id>
<phase>validate</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>I'm bound to validate phase.</echo>
</tasks>
</configuration>
</execution>
<execution>
<id>ant-verify</id>
<phase>verify</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<echo>I'm bound to verify phase.</echo>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
5、获取插件信息
(1)、在线插件信息
基本上所有主要的Maven插件都来自Apache和Codehaus。详细的列表可以访问地址:http://maven.apache.org/plugins/index.html
(2)、使用maven-help-plugin描述插件
例如,获取maven-compiler-plugin 2.1版本的信息:
mvn help: describe -D plugin = org.apache.maven.plugins : maven-compiler-plugin:2.1
注:这里的参数plugin, 需要输入插件的groupId、artifactId、version
6、从命令行调用插件
mvn命令的作用是激活生命周期阶段,从而执行那些绑定在生命周期阶段上的目标插件。
mvn命令的格式是:mvn [options] [<goal(s)>] [<phase(s)>]
options表示可用的选项,goal表示插件目标,phase表示生命周期阶段。
此外,对于有些不适合绑定在生命周期上的插件目标,例如mave-help-plugin:describe,Maven还支持直接从命令行调用插件目标。调用方式如下:
mvn help:describe -D plugin = compiler
mvn dependency:tree
与如下命令作用一样:
mvn org.apache.maven.plugins:maven-help-pluginL2.1:describe -D plugin = compiler
mvn org.apache.maven.plugins:maven-dependency-plugin:2.1:tree
这里需要注意的是目标前缀(Goal Prefix)的概念,其作用是方便在命令行直接运行插件。help是maven-help-plugin的目标前缀,dependency是maven-dependency-plugin的前缀。
7、插件解析机制
(1)、插件仓库
Maven是会区别对待依赖的远程仓库和插件的远程仓库的。在POM或settings.xml文件中,repositories及其repository子元素对应的是依赖的远程仓库,pluginRepositories、pluginRepository子元素配置对应的插件远程仓库。
(2)插件的默认groupId
在配置插件时,如果是Maven官方的插件,就可以省略groupId配置,因为Maven在解析这样的插件时,会自动用默认groupId org.apache.maven.plugins补齐。
(3)解析插件版本
为简化插件配置和使用,在用户没有提供插件版本的情况下,Maven会自动解析插件版本。Maven在超级POM中为所有核心插件设定了版本,如果不是核心插件,其解析原理与仓库解析部分描述的一样,maven会归并本地仓库和所有远程仓库在该路径下的仓库元数据,就能计算出latest、release值。
注意:尽管maven可以解析插件版本,但推荐使用显式的版本设定,避免版本变化带来的项目构建失败。
(4)解析插件前缀(目标前缀)
maven从插件前缀解析得到插件坐标的原理如下:
首先,插件前缀与groupId:artifactId是一一对应的,这种匹配关系存储在仓库元数据中(注意是:groupId/maven-metadata.xml中);其次,groupId是默认或配置的,默认的两个是org.apache.maven.plugins、org.codehaus.mojo,配置的是指可以在settings.xml中配置让Maven检查其他的groupId,配置格式如下:
<settings>
<pluginGroups>
<pluginGroup>com.your.plugins</pluginGroup>
</pluginGroups>
</settings>
然后,就可以根据解析插件版本的方法,解析出版本。如果所有元数据中都不包含该插件前缀,则报错。
示例:dependency:tree
maven首先基于默认的groupId归并所有插件仓库的元数据org.apache/maven/plugins/maven-metadta.xml;其次检查归并后的元数据,找到对应的artifactId为maven-dependency-plugin;然后结合当前元数据的groupId org.apache.maven.plugins,使用版本解析方法解析得到version,这样就得到完整的插件坐标。
六、聚合和继承
1、聚合
一般的项目都包含多个模块,为了方便一次构建项目下的所有模块,而不是一个一个模块地来构建项目。Maven提供了聚合特性。
聚合模块是为了多模块项目的聚合功能而存在的,其仅有一个pom.xml文件,它仅仅是帮助聚合其他模块构建的工具,本身并没有实质内容。
聚合模块的pom.xml文件中的packaging值必须为pom,否则无法构建;
聚合模块的最核心配置时modules,可以声明任意数量的module元素,每个module的值都是一个当前POM的相对目录,代表一个模块;
聚合模块可以与其他模块是父子目录关系,也可以是平行目录关系,一般为了方便用户构建项目,通常将聚合模块放在项目目录的最顶层,其他模块则作为聚合模块的子目录存在。
2、继承
对于多模块项目来讲,不仅有聚合POM, 还有各个模块的POM, 通常这些POM都有许多相同的配置,重复往往就意味着更多的劳动和更多的潜在问题,为了避免POM配置的重复,Maven提供了继承机制。
所谓,POM的继承,就是创建POM的父子结构,然后父POM中声明的配置可供子POM继承,以实现“一处声明,多处使用”的目的。
父模块的POM,其打包方式(packaging)必须为pom。而且由于父模块只是为了帮助消除配置的重复而存在,因此它除了POM之外不包含任何其他文件。
例如,有如下父模块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>
</project>
子POM如果要继承该POM的内容,需要以下的元素声明:
<parent>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../account-parent/pom.xml</relativePath>
</parent>
其中,parent元素下的子元素groupId、artifactId和version指定了父模块的坐标,这三个坐标是必须的。元素relativePath表示父模块POM的相对路径。
注:relativePath的默认值是../pom.xml,也就是说,Maven默认父POM在上一层目录下。
实现上述步骤后,子POM的配置就可以简化了,例如:坐标声明,可以省略groupId、version,
在子POM中坐标声明需要指定artifact、name,而对于groupId、version则可以省略,因为该子模块隐式地从父模块继承了这两个元素。如果用户需要子模块是不同的groupId或version,直接在POM中显示声明groupId、version就可以了,这样可以覆盖掉父类的声明。
注:一般常常将聚合模块和父模块合并为一个(即将聚合POM文件与继承中的父POM文件合并),这样既简化了配置,也方便维护。
(1)、可继承的POM元素
【】groupId : 项目组ID
【】version:项目版本
【】description:项目的描述信息
【】organization:项目的组织信息
【】inceptionYear:项目的创始年份
【】url:项目的URL地址
【】developers:项目的开发者信息
【】contributors:项目的贡献者信息
【】distributionManagement:项目的部署配置
【】issueManagement:项目的缺陷跟踪系统信息
【】ciManagement:项目的持续集成系统信息
【】scm:项目的版本控制系统信息
【】mailingLists:项目的邮件列表信息
【】propeerties:自定义的Maven属性
【】dependencies:项目的依赖配置
【】dependencyManagement:项目的依赖管理配置
【】repositories:项目的仓库配置
【】build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等
【】reporting:包括项目的报告输出目录配置、报告插件配置等。
(2)、依赖管理
如上所列,dependencies元素可以被继承,但是一般不直接将子模块的共同依赖放置到父模块中,因为当增加新的子模块时,新的子模块就可能会从父模块中继承不必要的依赖。
为了解决上述问题,为了更加灵活地消除依赖重复配置,Maven提供的dependencyManagement元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。
在父模块的dependencyManagement声明的依赖既不会给父模块引入依赖,也不会给它的子模块引入依赖,但是该配置会被继承,当父模块POM中,声明的dependencyManagement提供了依赖的完整坐标,子模块的POM中,就可以省略version,仅仅配置依赖的groupId、artifact。
虽然,这种依赖管理机制不能减少太多的POM配置,不过还是建议采用这种方法,因为在父POM中使用dependencyManagement声明依赖能够统一项目范围中依赖的版本,降低依赖冲突的几率。
如果子模块不声明依赖的使用,即使该依赖已经在父POM的dependencyManagement中声明了,也不会产生任何实际的效果。
(3)、复制配置
名为import的依赖范围,仅在dependencyManagement元素下才有效果,使用该范围的依赖通常指向一个POM,作用是将目标POM中的dependencyManagement配置导入并合并到当前POM的dependencyManagement元素中。这样就达到了复制配置的目的。
(4)、插件管理
与依赖管理dependencyManagement类似,Maven提供了插件管理pluginManagement。在该元素中配置的插件不会造成实际的插件调用行为,当POM中配置了真正的plugin元素,并且其groupId、artifactId与pluginManagement中配置的插件匹配时,pluginManagement的配置才会影响实际的插件行为。
3、聚合与继承的关系
聚合主要是为了方便快速构建项目,继承主要是为了消除重复配置。
注:现实项目中,常常一个POM既是聚合POM,又是父POM。
4、约定优于配置
Maven最核心的设计理念之一,就是提倡“约定优于配置”(Convention Over Configuration)。
使用约定虽然会牺牲一部分灵活性,但是可以大量减少配置。
Maven的项目约定:
【】源码目录为src/main/java/
【】编译输出目录为target/classes/
【】打包方式为jar
【】包输出目录为target/
注:Maven也允许自定义目录,例如自定义源码目录,示例如下:
<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</groupId>
<artifactId>my-project</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Account Parent</name>
<build>
<sourceDirectory>src/java</sourceDirectory>
</build>
</project>
上述示例中,源码目录变成了src/java而不是默认的src/main/java。
注意,一般在处理遗留代码,并且没办法更改原来的目录结构时,才会自定义项目的目录配置。
如下是Maven官网描述的标准项目目录:https://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html
5、超级POM
任何一个Maven项目都隐式地继承自超级POM,大量的超级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的主要内容:
【】定义仓库及插件仓库,两者的地址都是中央仓库http://repo1.maven.org/maven2, 并且关闭了SNAPSHOT的支持
【】定义了项目的主输出目录、主代码输出目录、最终构件的名称格式、测试代码输出目录、主源码目录、脚本源码目录、测试源码目录、主资源目录和测试资源目录。
【】定义核心插件(设定了版本)
【】定义项目报告输出目录、项目发布的profile等。
6、反应堆(Reator)
在一个多模块的Maven项目中,反应堆(Reactor)是指所有模块组成的一个构建结构,对于单模块的项目,反应堆就是该模块本身。
(1)、反应堆的构建顺序
Maven会在聚合POM中的modules元素中按moudle声明顺序,按序读取POM,如果该POM没有依赖模块,那么就构建该模块,否则就先构建其依赖模块,如果该依赖还依赖于其他模块,则进一步先构建依赖的依赖。
(2)、裁剪反应堆
当项目庞大、模块特别多的时候,通过裁剪反应堆,可以跳过无须构建的模块,从而加速构建。
Maven提供了很多命令行选项支持实时裁剪反应堆:
【】-am, - -also -make 同时构建所列模块的依赖模块
【】-amd , -also -make -dependents :同时构建依赖于所列模块的模块
【】-pl, - -projects <arg> :构建指定的模块,模块间用逗号分隔
【】-rf , -resume -from <arg> : 从指定的模块开始执行反应堆
示例:
【】mvn clean install -pl account-email, account-persist // 构建account-email和account-persist模块
【】mvn clean install -pl account-email -am // 先构建account-email的依赖模块,再构建account-eamil模块
【】mvn clean install -pl account-parent -amd // 先构建account-parent模块,再构建所有依赖于account-parent的模块
【】mvn clean install -rf account-eamil // 在完整的反应堆构建顺序基础上指定从account-eamil模块开始构建
【】mvn clean install -pl account-parent -amd -rf account-eamil // 级联使用,在-pl account-parent -amd命令裁剪后的反应堆的基础上,执行-rf account-eamil命令。