除了坐标、依赖以及仓库以外,maven另外两个核心概念就是生命周期和插件。maven的生命周期是抽象的,其实际行为都是由插件来完成。
7.1 什么是生命周期
在maven出现之前,项目构建的生命周期就已经存在,软件开发人员每天都在对项目进行清理、编译、测试以及部署。
Maven的生命周期就是为了对所有的构建过程进行抽象和统一。Maven从大量项目和构建工具中学习和反思,然后总结出一套高度完善的、易扩展的生命周期。这个生命周期包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。也就是说,几乎所有项目的创建,都能映射到这样一个生命周期上。
Maven的生命周期是抽象的, 这意味着生命周期本身不做任何实际任务,在maven的设计中,实际的任务都交给插件来完成。这种思想与设计模式的模板方法(template method)非常相似。模板方法在父类中定义算法的整体结构,子类可以通过实现或者重写父类的方法来控制实际的行为,这样既保证了算法有足够的可扩展性,又能够严格控制算法的整体结构。如下的模板方法抽象类能够很好的体现maven生命周期的概念。
public abstract class AbstractBuild {
private AbstractBuild(){}
public void build() {
init();
compile();
test();
packagee();
integrationTest();
deploy();
}
/**
* 抽象方法 初始化
*/
protected abstract void init();
/**
* 抽象方法 编译
*/
protected abstract void compile();
/**
* 测试
*/
protected abstract void test();
/**
* 打包:因为package是关键词
*/
protected abstract void packagee();
/**
* 集成测试
*/
protected abstract void integrationTest();
/**
* 部署
*/
protected abstract void deploy();
}
这个类没有具体实现初始化,编译,测试等行为,它们都交由子类去实现。
虽然上述代码和Maven实际代码相去甚远,Maven的生命周期包含更多的步骤和更复杂的逻辑,但是它们的基本理念是相同的。为了不让用户重复发明*,Maven设计了插件机制。
每个构建步骤都可以绑定一个或多个插件行为,而且maven为大多数构建步骤编写并绑定了默认插件。例如,针对编译的插件有maven-compiler-plugin
,针对测试的插件有maven-surefire-plugin
等。当用户有特殊需要的时候,也可以配置插件定制构建行为,甚至自己编写插件。
maven定义的生命周期和插件机制一方面保证了所以Maven项目有一致的构建标准,另一方面又通过默认插件简化和稳定了实际项目的构建。此外,扩展性也不错。
7.2 生命周期详解
三套生命周期
Maven其实拥有三套相互独立的生命周期,他们分别为clean、default和site。clean生命周期的目的是清理项目,default生命周期的目的是构建项目,而site则是建立项目站点。
每个生命周期包含一些阶段(phase),这些阶段是有顺序的,且具有依赖的关系。
但是三套生命周期本身是相互独立的, 用户可以仅仅调用default生命周期的某个阶段,而不会对其他生命周期产生任何影响。
clean生命周期
clean生命周期的目的是清理项目,它包含三个阶段:
- pre-clean:执行一些清理前需要完成的任务
- clean:清理上一次构建生成的文件
- post-clean:执行一些清理后需要完成的工作
default生命周期
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:
- process-test-resources:
- 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:将生成的项目站点发布到服务器上。
命令行与生命周期
以常见命令解释其执行的生命周期阶段:
-
$mvn clean
:该命令调用clean生命周期的clean阶段。实际执行的阶段为clean生命周期的pre-clean
和clean
阶段。 -
$mvn test
:该命令调用default生命周期的test阶段。实际执行的阶段为default生命周期的validate
、initialize
等,直到test
的所有阶段。这也解释了为什么在执行测试的时候,项目的代码能够自动得以编译。 -
$mvn clean install
:该命令调用clean生命周期的clean阶段和default生命周期的install
阶段。实际执行的阶段为clean
生命周期的pre-clean
和clean
阶段,以及default生命周期从validate到install的全部阶段。该命令结合了两个生命周期,在执行正在的项目构建之前清理项目是一个很好的实践。
7.3 插件目标
Maven的核心仅仅定义了抽象的生命周期,具体的任务是交给插件完成的,插件以独立的构件形式存在,因此,maven核心的分发包只有3MB,Maven会在需要的时候下载并使用插件。
maven-dependency-plugin有十多个目标,每个目标对应一个功能,上述提到的几个功能分别对应dependency:analyze
,dependency:tree
和dependency:list
。
冒汗前面是插件前缀,冒号后面是该插件的目标。
类似的还有:surefire:test
这个是maven-surefile-plugin
的test目标。
7.4 插件绑定
Maven的生命周期与插件相互绑定,用以完成实际的构建任务。具体而言,是生命周期的阶段与插件的目标相互绑定,以完成某个具体的构建任务。
内置绑定
为了能让用户几乎不用任何配置就能构建Maven项目,Maven在核心为一些主要的生命周期阶段绑定了很多插件的目标,当用户通过命令行调用生命周期阶段的时候,对应的插件目标就会执行相应的任务。
clean生命周期仅有的pre-clean
、clean
和post-clean
三个阶段,其中的clean
与maven-clean-plugin:clean
绑定。
自定义绑定(重点)
除了内置绑定以外,用户还能够自己选择将某个插件绑定到生命周期的某个阶段,这种自定义绑定方法能让maven项目在构建过程中执行更多任务。
一个常见的例子是创建项目的源码jar包,内置的插件绑定关系中并没有涉及这一任务,因此需要用户自行配置。maven-source-plugin
可以帮助我们完成该任务,它的jar-no-fork
目标能够将项目的主代码打包成jar文件。
<build>
<plugins>
<plugin>
<GAV/>
<executions>
<execution>
<id>task1</id>
<phase>varify</phase>
<goals>
<goal/>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
在POM的build元素下的<plugins>
字元素中声明插件的使用。对于自定义绑定的插件,用户总是应该声明一个非快照版本,这样可以避免由于版本变化造成的构建不稳定性。
在上述配置中,除了基本的插件坐标声明外,还有插件执行配置,<executions>
下每个execution
资源时可以用来配置执行一个任务。该例子中配置了一个id为task1
的任务,通过phrase
配置,将其绑定到verify声明周期阶段上,再通过goals配置指定要执行的插件目标。至此,自定义插件绑定完成。运行mvn verify
就能看到如下输出。
我们知道,当插件目标被绑定到不同的生命周期阶段的时候,其执行顺序会由生命周期阶段的先后顺序决定。如果多个目标被绑定到同一个阶段,这些插件声明的先后顺序决定了目标的执行顺序。
7.5 插件配置
完成了插件和生命周期的绑定之后,用户还可以配置插件目标的参数,进一步调整插件目标所执行的任务,以满足项目的需求。
命令行插件配置
用户可以在maven命令中使用-D
参数,并伴随一个参数键=参数值的形式来配置插件目标的参数。
比如在maven-surefile-plugin
中提供了maven.test.skip
参数:
mvn install -Dmaven.test.skip=true
-D
是用来在启动一个java程序时设置系统属性值的。如果该值是一个字符串且包含空格,那么需要包在一对双引号中。
POM中插件全局配置
用户可以在声明插件的时候,对此插件进行一个全局的配置。例如我们需要配置maven-compiler-plugin
告诉它编译Java 1.5版本的源文件,生成与JVM1.5兼容的字节码:
<build>
<plugins>
<plugin>
<groupId>org.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
这样,不管绑定到compile阶段的maven-compiler-plugin:compile
任务,还是绑定到test-compiler
阶段的maven-compiler-plugin:testCompiler
任务,就都能够使用该配置,基于1.5版本进行编译。
POM中插件任务配置
略
7.6 获取插件信息
在线插件信息
基本上所有主要的maven插件都来自Apache和codehaus
网址:https://maven.apache.org/plugins/index.html
托管于Codehaus上的Mojo项目也提供了大量maven插件。但是该网址已经关停了。
使用maven-help-plugin描述插件
mvn help:describe -Dplugin = org.apache.maven.plugins:maven-compiler-plugin:2.1
执行了maven-help-plugin
的describe
目标。在参数plugin中输入需要描述插件的GAV。
【略】
常见插件
插件名称 | 用于 | 来源 |
---|---|---|
maven-clean-plugin | ||
maven-compiler-plugin | ||
deploy | ||
install | ||
resources | 处理资源文件 | |
maven-site-plugin | 生成站点 | |
maven-surefire-plugin | 执行测试 | |
maven-jar-plugin | 构建jar | |
maven-javadoc-plugin | 生成javadoc文档 | |
maven-pmd-plugin | 生成PMD报告 | |
maven-assembly-plugin | 构建自定义格式的分发包 | |
maven-enforcer-plugin | 定义规则并强制要求项目遵循 | |
maven-source-plugin | 生成源码包 | |
properties-maven-plugin | 从properties文件读写maven属性 | |
jetty-maven-plugin | 集成Jetty容器,实现快速开发测试 |
7.8 插件解析机制
为了方便用户使用和配置插件,maven不需要用户提供完整的插件坐标信息,就可以解析得到正确的插件,maven的这一特性是一把双刃剑,虽然它简化了插件的使用和配置,可一旦插件的行为出现异常,用户就很难快速定位到出现问题的构建。比如
mvn help:system
这一条命令,它到底执行了什么插件?下面介绍原理。
插件仓库
与依赖构件一样,插件构件同样基于坐标存储在maven仓库中。在需要的时候,maven会从本地查找,如果没有去远程操作找。
但是maven会区别对待依赖的远程仓库和插件的远程仓库。当maven需要的依赖在本地仓库不存在时,它会去所配置的远程仓库查找,可是当maven需要的插件在本地仓库不存在时,它就不会去这些远程仓库查找。
插件的远程仓库使用pluginRepositories和pluginRepository配置。
插件的默认groupId
在POM中配置插件的时候,如果该插件是maven的官方插件,就可以忽略groupId配置。
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
上述配置就省略了groupId,maven在解析该插件的时候,会自动用默认groupId:org.apache.maven.plugins补齐。【不推荐】
解析插件版本
同样是为了简化插件的配置和使用,在用户没有提供版本的情况下,maven会自动解析插件版本。
首先,maven在超级POM中为所有核心插件设定了版本,超级POM是所有maven项目的父POM。所有项目都继承这个超级POM配置。因此,即使用户不加任何配置,maven使用核心插件的时候,它们的版本就已经确定了。
如果用户使用某个插件时,没有设定版本,而这个插件又不属于核心插件的范畴,maven就会去检查所有仓库中可用的版本,然后做出选择。
maven遍历本地仓库和所有远程插件仓库,将该路径下的仓库元数据归并后,就能计算出latest和release的值。然后使用release而不是latest,避免由于快照频繁更新导而导致的插件行为不稳定。
【但是不设定版本是不推荐的, 同样超级POM中为核心插件已经设定了版本】
解析插件前缀
插件前缀与groupId:artifactoryId
是一一对应的,这种匹配关系存储在仓库元数据中。与依赖的groupId/artifactoryId/maven-metadata.xml
不同,这里的仓库元数据为groupId/maven-metadata.xml
。
Maven在解析插件仓库元数据的时候,会默认使用org.apache.maven.plugins
和org.codehaus.mojo
两个groupId。
也可以通过配置settings.xml让maven检查其他groupId上的插件仓库元数据:
<settings>
<pluginGroups>
<pluginGroup>com.ssozh.plugins</pluginGroup>
</pluginGroups>
</settings>
基于该配置,maven就不仅仅会检查这两个地方的xml了。还会检查com/ssozh/plugins/maven-metadata.xml
。
<metadata>
<plugins>
<plugin>
<name>Maven Dependency Plugin</name>
<prefix>dependency</prefix>
<artifactId>mavne-dependency-plugin</artifactId>
</plugin>
</plugins>
</metadata>
上述内容中可以看出,当Maven解析到dependency:tree
这样的命令的时候,它首先基于默认的groupId
归并所有插件仓库的元数据,其次检查归并后的元数据,找到对应的artifactId
。然后结合当前元数据的groupId
。最后使用上述方法获取version
。这时就得到了完整的插件坐标。
如果该maven-metadata.xml
没有记录该插件前缀,就去mojo
然后去自定义,如果都没有,则报错。
7.9 小结
本章介绍了maven的生命周期和插件这两个重要的概念。不仅解释了生命周期背后的理念,还详细阐述了clean、default、site三套生命周期各自的内容。此外,本章还重点介绍了maven插件如何与生命周期绑定,以及如何配置插件行为,如何获取插件信息。
补充
generate-source: