Maven 的坐标和依赖
坐标详解
依赖管理是 Maven 的一大功能,为了能够自动化地解析任何一个 Java 构件,Maven 就必须将它们唯一标识,这就是依赖管理的底层基础——坐标。Maven 的坐标是通过一些元素定义的,它们是 groupId、artifictId、version、packaging 和 classifier。先看一组坐标定义:
<groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.47</version> <packaging>jar</packaging>
这是 JSON 框架 fastjson 的坐标定义。在代码片段中,其坐标为:groupId:com.alibaba、artifactId:fastjson、version:1.2.47、packaging:jar,没有指定 classifier。下面说明各个坐标元素:
-
groupId:定义当前 Maven 项目隶属的组织或实际项目。如果 Maven 项目没有划分成多个模板,那么 groupId 一般定义成 Maven 项目关联的组织或公司,如 fastjson 的 groupId 是 com.alibaba。如果 Maven 项目划分成多个模块,如 Apache Commons 包含了 commons-lang3、commons-io 等模块,那么 groupId 一般是定义成 Maven 项目隶属的实际项目,如 commons-lang3 的 groupId 是 org.apache.commons。
-
artifictId:定义项目在组中的唯一标识,一般是项目名或模块名。如果 Maven 项目没有划分成多个模板,那么 artifictId 一般定义成项目名,如 fastjson。如果 Maven 项目划分成多个模块,那么 artifictId 一般定义成模块名,如 commons-lang3。
-
version:定义 Maven 项目的版本号。
-
packaging:定义 Maven 项目的打包方式。打包方式通常与所生成构件的文件扩展名相对应,如上例中 packaging 为 jar,最终构件的文件名为 fastjson-1.2.47.jar。而使用 war 打包方式的 Maven 项目,最终生成的构件通常会有一个 .war 文件。打包方式还会影响到构建的生命周期,比如 jar 打包和 war 打包会使用不同的命令。如果没有定义 packaging 元素,则默认为 jar。
-
classifier:定义构建输出的一些附属构件。附属构件与主构件对应,如上例中的主构件是 fastjson-1.2.47.jar,该项目可能还会通过使用一些插件生成源代码和文档,如 fastjson-1.2.47-sources.jar、fastjson-1.2.47-javadoc.jar,这些就是附属构件。这样,附属构件也就拥有了自己唯一的坐标。注意,classifier 不能直接定义,因为附属构件不是项目直接默认生成的,而是由附加的插件帮助生成。
上述五个元素中,groupId、artifictId 和 version 是必须定义的, packaging 是可选的(默认为 jar),而 classifier 是不能直接定义的。
项目构件的文件名与坐标是相对应,一般规则为 artifictId-version[-classifier].packaging,[-classifier] 表示可选。例如上例 fastjson 的主构件为 fastjson-1.2.47.jar,附属构件有 fastjson-1.2.47-sources.jar。需要注意的一点是 packaging 并非一定与构件的扩展名对应,比如 packaging 为 maven-plugin 的构件扩展名为 jar。
依赖的配置
Maven 项目通过在 POM 配置 dependencies 元素来管理依赖,这样 Maven 就会根据配置自动下载所需的依赖构件, dependencies 可包含如下内容:
<project > ... <dependencies> <dependency> <groupId>...</groupId> <artifactId>...</artifactId> <version>...</version> <type>...</type> <scope>...</scope> <optional>...</optional> <exclusions> <exclusion> <groupId>...</groupId> <artifactId>...</artifactId> </exclusion> </exclusions> </dependency> </dependency> <dependency> ... </dependency> </dependencies> ... </project>
dependencies 可以包含一系列 dependency 元素,每个 dependency 表示引入一个依赖,dependency 可包含的元素有:
-
groupId、artifactId、version:依赖的基本坐标。
-
type:依赖的类型,对应于项目坐标定义的 packaging。通常情况下,该元素不必声明,默认值为 jar。
-
scope:依赖范围。
-
optional:标记依赖是否可选。
-
exclusions:用于排除传递性依赖。
依赖范围
Maven 在编译、测试、运行项目时,使用的 classpath 是不同的。Maven 在编译项目主代码的时候使用一套 classpath,执行测试的时候会使用另一套 classpath,最后在实际运行项目时,又会使用一套 classpath。依赖范围就是用来控制依赖与这三种 classpath(编译 classpath、测试 classpath、运行 classpath)的关系。Maven 有以下几种依赖范围:
-
compile:编译依赖范围。如果没有指定买就会默认使用该依赖范围。使用此依赖范围的 Maven 依赖,对于编译、测试、运行三种 classpath 都有效。
-
test:测试依赖范围。使用此依赖范围的 Maven 依赖,只对于测试 classpath 有效,在编译主代码或者运行项目时将无法使用此依赖。典型的例子是 JUnit,它只有在编译测试代码及运行测试代码的时候才需要。
-
provided:已提供依赖范围。使用此依赖范围的 Maven 依赖,对于编译和测试 classpath 有效,但在运行时无效。典型的例子是 servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要 Maven 重复引入。
-
runtime:运行时依赖范围。使用此依赖范围的 Maven 依赖,对于测试和运行 classpath 有效,但在编译主代码的时候无效。典型的例子是 JDBC 驱动实现,项目主代码但编译只需要 JDK 提供的 JDBC 接口,只有在执行测试或者运行项目的时候才需要 JDBC 驱动的实现类。
-
system:系统范围依赖。该依赖与三种 classpath 的关系,和 provided 依赖范围完全一致。但是,使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖构件的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。systemPath 元素可以引用环境变量,如:
<dependency> <groupId>javax.sql</groupId> <artifactId>jdbc-stdext</artifactId> <version>2.0</version> <scope>system</scope> <systemPath>${java.home}/lib/rt.jar</systemPath> </dependency>
传递性依赖
假设 Maven 项目引入了构件 A,而构件 A 又依赖于构件 B,那么 Maven 项目会自动引入构件 B,这就是传递性依赖。例如,spring-core 构件依赖于 commons-logging 构件,如果项目引入了 spring-core 依赖,Maven会自动将 commons-logging 引入,开发者无需再手动去加入 commons-logging 依赖。
依赖调解
Maven 的传递性依赖机制,不仅简化了依赖声明,而且使用者只需关心项目的直接依赖是什么。但有时候,当传递性依赖造成问题时,我们就需要清楚该传递性依赖是从哪条依赖路径引入的。
例如,项目 A 有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0),X 是 A 的传递性依赖,但是两条依赖路径上的 X 版本不一样,那么哪个版本的 X 会被 Maven 解析使用呢?Maven 依赖调解的第一原则是:路径最近者优先。在这个例子中,X(1.0) 的路径长度为 3,而 X(2.0) 的长度为 2,因此 X(2.0) 会被解析使用。
如果有依赖路径相等呢?比如有这样的依赖关系:A->B->Y(1.0)、A->C->Y(2.0),Y(1.0) 和 Y(2.0) 的依赖路径长度一样,那么哪一个会被解析使用呢?Maven 依赖调解的第二原则是:最先声明者优先。在依赖路径长度相等的情况下,在 POM 中依赖声明的顺序决定了谁会被解析使用。在这个例子中,如果 B 的依赖声明在 C 之前,那么 Y(1.0) 就会被解析使用。
排除依赖
传递性依赖会给项目隐式地引入很多依赖,极大地简化了项目依赖的管理,当依赖调解选择的版本不是我们所期待的,比如引入的是一个比较旧的版本,而项目的代码使用了新的特性,需要引入的是较新的版本。这时,可以使用 exclusions 元素声明排除传递性依赖,然后再显示地声明所需版本的依赖。exclusions 元素可以包含一个或多个 exclusion 子元素,因此可以排除一个或多个传递性依赖。
Maven 仓库
Maven 的坐标和依赖是任何一个构件在 Maven 世界中的逻辑表示,而构件的物理表示是文件,Maven 通过仓库来统一管理这些文件。
任何一个构件都有其唯一的坐标,根据这个坐标可以定义其在仓库中的唯一存储路径,该路径与坐标大致对应关系为 groupId/artifactId/version/artifact-version.packaging。例如,org.jsoup:jsoup:1.8.3 这一依赖,其对应的仓库路径为 org/jsoup/jsoup/1.8.3/jsoup-1.8.3.jar。
仓库的分类
对于 Maven 来说,仓库只分为两类:本地仓库和远程仓库。当 Maven 根据坐标寻找构件的时候,它首先会查看本地仓库,如果本地仓库存在此构件,则直接使用;如果本地仓库不存在此构件,Maven 就会去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。如果本地仓库和远程仓库都没有需要的构件,Maven 就会报错。
在这个最基本的分类的基础上,还有一些特殊的远程仓库。中央仓库是 Maven 核心自带的远程仓库,它包含了绝大部分开源的构件。
私服是另一种特殊的远程仓库,为了节省带宽和时间,应该在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库。内部的项目还能部署到私服上供其他项目使用。
除了中央仓库和私服,还有很多其他公开的远程仓库,比如阿里云的 Maven 仓库。
本地仓库
默认情况下,不管是在 Windows、Linux 还是 MacOS 上,每个用户在自己的用户目录下都有一个路径为 .m2/repository 的仓库目录。
有时用户会想要自定义本地仓库的地址,那么可以通过编辑 settings.xml 文件,设置 localRepository 元素的值为想要的仓库地址,例如:
<settings> ... <localRepository>/Users/huey/maven/repository</localRepository> ... </settings>
一个构件只有在本地仓库中,才能供其他 Maven 项目使用,那么构件如何进入到本地仓库呢?最常见到是依赖 Maven 从远程仓库下载到本地仓库中。还有一种常见到情况是,将本地项目到构件安装到 Maven 仓库中。例如,本地有两个项目 A 和 B,两者都无法从远程仓库获得,而同时 A 又依赖于 B,为了构建 A,B 就必须首先构建并安装到本地仓库中。
可以在某个项目中执行 mvn clean install
命令,使用 install 插件的 install 目标将项目构建的输出文件安装到本地仓库中。
远程仓库
前面已经提到,当 Maven 无法从本地仓库查找到需要到构件时,它就会从远程仓库下载构件至本地仓库。每个用户只有一个本地仓库,但可以配置多个远程仓库。
中央仓库
由于最原始但本地仓库是空的,Maven 必须知道至少一个可用的远程仓库,才能在执行 Maven 命令的时候下载到需要的构件。中央仓库就是这样一个默认的远程仓库,Maven 的安装文件自带了中央仓库的配置。使用解压工具或者反编译工具打开 $M2_HOME/lib/maven-model-builder-3.5.3.jar,然后访问 org/apache/maven/model/pom-4.0.0.xml,可以观察到有如下配置:
<repositories> <repository> <id>central</id> <name>Central Repository</name> <url>https://repo.maven.apache.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories>
这个配置文件 pom-4.0.0.xml 是所有的 Maven 项目都会继承的超级 POM。上述配置使用 central 对中央仓库进行唯一标识,仓库的地址是 https://repo.maven.apache.org/maven2。
私服
私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的 Maven 用户使用。当 Maven 需要下载构件的时候,它从私服请求,如果私服上不存在该构件,则从外部的远程仓库下载,缓存在私服上之后,再为 Maven 的下载请求提供服务。此外,一些无法从外部仓库下载到的构件也能从本地上传到私服供其他 Maven 项目使用。
配置远程仓库
在很多情况下,默认的中央仓库无法满足项目的需求,可能项目需要的构件存在于其他的远程仓库中。这时,可以在 POM 中配置该仓库,如下所示:
<project> ... <repositories> <repository> <id>aliyun</id> <name>Aliyun Maven Repository</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> ... </project>
在 repositories 元素下,使用 repository 子元素声明一个或多个远程仓库。在上述的例子中,声明了一个 id 为 aliyun,名称为 Aliyun Maven Repository 的仓库。任何一个仓库声明的 id 必须是唯一的,尤其需要注意的是,默认的中央仓库使用的 id 是 central,如果其他的仓库声明也使用这个 id,就会覆盖中央仓库的配置。url 子元素的值指向了仓库的地址,通常情况下,仓库的地址都是基于 http 协议,Maven 用户可直接在浏览器访问仓库地址浏览构件。
releases 和 snapshots 元素是用来控制 Maven 对于发布版构件和快照版构件的下载。releases 的子元素 enabled 值是 true,表示开启远程仓库的发布版本的下载支持,而 snapshots 的 enabled 值是 false,表示关闭远程仓库的快照版本的下载支持。因此,根据该配置,Maven 只会从阿里云仓库下载发布版本的构件,而不会下载快照版本的构件。
对于 releases 和 snapshots 元素,除了 enabled,它还包含两个子元素 updatePolicy 和 checksumPolicy:
<snapshots> <enabled>true</enabled> <updatePolicy>daily</updatePolicy> <checksumPolicy>ignore</checksumPolicy> </snapshots>
updatePolicy 元素用于配置 Maven 从远程仓库检查更新的频率,它的取值可以是:
-
daily - 默认值,表示 Maven 每天检查一次。
-
never - 从不检查更新。
-
always - 每次构建都检查更新。
-
interval:X - 每隔 X 分钟检查一次更新。
checksumPolicy 元素用于配置 Maven 检查校验和文件的策略。当构件部署到 Maven 仓库中时,会同时部署对应的校验和文件。在下载构件时,Maven 会检查校验和文件,当校验和验证失败时,Maven 会根据策略做不同的处理:
-
warn - 默认值,在执行构建时输出警告信息。
-
fail - 遇到校验和错误就构建失败。
-
ignore - 完全忽律校验和错误。
远程仓库的认证
大部分远程仓库无须认证就可以访问,但有时出于安全方面的考虑,需要提供认证信息才能访问一些远程仓库。配置认证信息和配置仓库信息不同,仓库信息可以直接配置在 POM 文件中,但是认证信息必须配置在 settings.xml 文件中。
下面的代码演示了为一个 id 为 myrepo 的仓库配置认证信息:
<settings> ... <servers> <server> <id>myrepo</id> <username>test</username> <password>test#123</password> </server> </servers> ... </settings>
部署至远程仓库
Maven 除了能对项目进行编译、测试、打包之外,还能将项目生成的构件部署到仓库中。配置示例如下:
<project> ... <distributionManagement> <repository> <id>releases</id> <name>Release Repository</name> <url>http://192.168.1.100/nexus/content/repositories/releases</url> </repository> <snapshotRepository> <id>snapshots</id> <name>Snapshot Repository</name> <url>http://192.168.1.100/nexus/content/repositories/snapshots</url> </snapshotRepository> </distributionManagement> ... </project>
distributionManagement 元素包含了 repository 和 snapshotRepository 子元素,前者表示发布版本构件的仓库,后者表示快照版本的仓库。这两个元素都要配置 id、name、url,id 为远程仓库的唯一标识,name 是仓库的名称,url 是仓库的地址。
部署构件到远程仓库通常需要认证信息,配置认证的方式已在上一节介绍。
配置正确后,在命令行执行 mvn clean deploy
,Maven 就会将项目构建输出的构件部署到配置对应的远程仓库。如果项目当前的版本是快照版本,则部署到快照版本仓库地址,否则就部署到发布版本仓库的地址。
镜像
如果仓库 X 可以提供仓库 Y 存储的所有内容,那么就可以认为 X 是 Y 的一个镜像。也就是说,任何一个可以从仓库 Y 获得的构件,都能够从它的镜像中获取。例如,http://maven.aliyun.com/nexus/content/groups/public/ 可以认为是中央仓库 https://repo.maven.apache.org/maven2/ 的一个镜像,由于地理位置的因素,该镜像往往能够提供比中央仓库更快的服务。因此,可以配置 Maven 使用该镜像来替代中央仓库:
<settings> ... <mirrors> <mirror> <id>aliyun</id> <name>Nexus Public Mirror</name> <mirrorOf>central</mirrorOf> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> </mirror> </mirrors> ... </settings>
上述的例子中,mirrorOf 的值是 central,表示该配置为中央仓库的镜像,任何对于中央仓库的请求都会转至该镜像。另外三个子元素 id、name、url 与其他仓库配置一样,表示仓库的唯一标识、名称和地址。类似地,如果镜像需要认证,也是基于该 id 配置仓库认证。
Maven 的生命周期和插件
Maven 的生命周期是用于对构建过程进行抽象和统一,生命周期包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。Maven 的生命周期是抽象的,即生命周期本身不做任何实际的工作,在 Maven 的设计中,实际的任务是由插件完成的。
三套生命周期
Maven 拥有三套相互独立的生命周期,它们分别为 clean、default 和 site。clean 生命周期的目的是清理项目,default 生命周期的目的是构建项目,而 site 生命周期的目的是建立项目站点。
每个生命周期包含一些阶段,这些阶段是有顺序的,并且后面的阶段依赖于前面的阶段。以 clean 生命周期为例,它包含的阶段有 pre-clean、clean 和 post-clean。当用户调用 pre-clean 时,只有 pre-clean 阶段得以执行;当用户调用 clean 时,pre-clean 和 clean 阶段会得以顺序执行;当用户调用 post-clean 时,pre-clean、clean 和 post-clean 阶段会得以顺序执行。
较之于生命周期阶段的前后依赖关系,三套生命周期本身是相互独立的。用户可以仅仅调用 clean 生命周期的某个阶段,或者仅仅调用 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 本地仓库。
-
deploy:将最终的包部署到远程仓库。
site 生命周期
site 生命周期的目的是建立和发布项目的站点,Maven 能够基于 POM 所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。该生命周期包含如下阶段:
-
pre-site:执行一些在生成项目站点之前需要完成的工作。
-
site:生成项目站点文档。
-
post-site:执行一些在生成项目站点之后需要完成的工作。
-
site-deploy:将生成的项目站点发布到服务器上。
命令行与生命周期
从命令行执行 Maven 任务的最主要方式就是调用 Maven 的生命周期阶段。需要注意的是,各个生命周期是相互独立的,而一个生命周期的阶段是有前后依赖关系的。下面以一些常见的 Maven 命令为例,解释其执行的生命周期阶段。
-
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 的所有阶段。该命令结合了两个生命周期,在执行真正的项目构建之前清理项目是一个很好的实践。
-
mvn clean deploy site-deploy
- 该命令调用 clean 生命周期的 clean 阶段、default 生命周期的 deploy 阶段和 site 生命周期的 site-deploy 阶段。实际执行的阶段为 clean 生命周期的 pre-clean、clean 阶段,default 生命周期的所有阶段,以及 site 生命周期的所有阶段。该命令结合了三个生命周期。
插件目标
Maven 的插件是以独立的构件形式存在的,Maven 会在需要的时候下载并使用插件。对于插件本身,为了能够复用代码,它往往能够完成多个任务。例如 maven-dependency-plugin,它能够基于项目依赖做很多任务。它能够分析项目依赖,帮助找出潜在的无用依赖;它能够列出项目的依赖树,帮助分析依赖来源;它能够列出项目所有已解析的依赖等等。为每个这样的功能编写一个独立的插件显然是不可取的,因为这些任务背后有很多可以复用的代码,因此,这些功能聚集在一个插件里,每个功能就是一个插件目标。
maven-dependency-plugin 有十多个目标,每个目标对应了一个功能,上述提到的几个功能分别对应的插件目标为 dependency:analyze、dependency:tree 和 dependency:list。这是一种通用的写法,冒号前面是插件前缀,冒号后面是该插件的目标。类似地,还可以写出 compiler:compile(这是 maven-compiler-plugin 的 compile 目标)和 surefire:test(这是 maven-surefire-plugin 的 test 目标)。
插件绑定
Maven 的生命周期与插件相互绑定,用以完成实际的构建任务。具体而言,是生命周期的阶段与插件的目标相互绑定,以完成某个具体的构建任务。例如项目编译这一任务,它对应了 default 生命周期的 compile 这一阶段,而 maven-compiler-plugin 这一插件的 compile 目标能够完成该任务,因此,将它们绑定,就能实现项目编译的目的。
内置绑定
为了能让用户几乎不用任何配置就能构建 Maven 项目,Maven 核心为一些主要的生命周期阶段绑定了很多插件的目标,当用户通过命令行调用生命周期阶段的时候,对应的插件目标就会执行相应的任务。
clean 生命周期阶段与插件目标的绑定关系如下表所示:
生命周期阶段 | 插件目标 |
pre-clean |
|
clean | maven-clean-plugin:clean |
post-clean |
|
site 生命周期阶段与插件目标的绑定关系如下表所示:
生命周期阶段 | 插件目标 |
pre-site |
|
site | maven-site-plugin:site |
post-site |
|
site-deploy | maven-site-plugin:deploy |
相对于 clean 和 site 生命周期,default 生命周期与插件目标的绑定关系显得比较复杂一些。这是因为对于任何项目来说,比如 jar 项目和 war 项目,它们的项目清理和站点生成任务是一样的,不过构建过程则有区别。例如 jar 项目需要打成 JAR 包,而 war 项目需要打成 WAR 包。
由于项目的打包类型会影响构建的具体过程,因此,default 生命周期的阶段与插件目标的绑定关系由项目的打包类型所决定。最常见的打包类型是 jar,它也是默认的打包类型,其 default 生命周期阶段与插件目标的绑定关系如下表所示:
生命周期阶段 | 插件目标 |
process-resources | maven-resources-plugin:resources |
compile | maven-compiler-plugin:compile |
process-test-resources | maven-resources-plugin:testResources |
test-compile | maven-compiler-plugin:testCompile |
test | maven-surefire-plugin:test |
package | maven-jar-plugin:jar |
install | maven-install-plugin:install |
deploy | maven-deploy-plugin:deploy |
注意,上表只列出了拥有插件绑定关系的阶段,default 生命周期还有很多其他阶段,默认它们没有绑定任何插件,因此也没有任何实际行为。
除了默认的打包类型 jar 之外,常见的打包类型还有 war、pom 等,它们的 default 生命周期与插件目标的绑定关系可参考 Maven 的官方文档。
自定义绑定
除了内置绑定,用户还可以自己选择将某个插件目标绑定到生命周期到某个阶段上。一个常见的例子是创建项目的源码的 jar 包,内置的插件绑定关系并没有涉及这一任务,因此需要用户自行配置。maven-source-plugin 可以完成该任务,它的 jar-no-fork 目标能够将项目的主代码打包成 jar 文件,可以将其绑定到 default 生命周期的 verify 阶段上,在执行完集成测试后和安装构件之前创建源码的 jar 包。配置如下所示:
<project> ... <build> <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> </build> ... </project>
在 POM 的 build 元素下的 plugins 子元素声明一个或多个插件的使用,该例子使用的是 maven-source-plugin,其 groupId、artifactId、version 定义了插件的基本坐标,executions 下每个 execution 子元素可以用来配置执行一个任务。该例中配置了一个 id 为 attach-sources 的任务,通过 phrase 配置,将其绑定到 verify 声明周期阶段上,再通过 goal是 配置指定要执行的插件目标。至此,自定义插件绑定完成,运行 mvn verify
命令,可以观察到如下输出:
[INFO] --- maven-source-plugin:2.1.1:jar-no-fork (attach-sources) @ hello-world --- [INFO] Building jar: /Users/huey/eclipse-workspace/hello-world/target/hello-world-0.0.1-SNAPSHOT-sources.jar
可以看到,当执行 verify 生命周期阶段当时候,maven-source-plugin:jar-no-fork 会得以执行,它会创建一个以 -sources.jar 结尾当源码文件包。
有时候,即使不通过 phase 元素配置生命周期阶段,插件目标也能够绑定到生命周期中去。例如,可以试着删除上述配置中到 phase 一行,再次执行 mvn verify
,仍然可以看到 maven-source-plugin:jar-no-fork 得以执行。这是因为有很多插件到目标在编写时已经定义了默认绑定阶段。可以使用 maven-help-plugin 查看插件详细信息,了解插件目标到默认绑定阶段:
$ mvn help:describe -Dplugin=org.apache.maven.plugins:maven-source-plugin:2.1.1 -Ddetail
该命令输出对应插件到详细信息,在输出信息中,可以看到关于目标 jar-no-fork 的如下信息:
... source:test-jar-no-fork Description: This goal bundles all the test sources into a jar archive. This goal functions the same as the test-jar goal but does not fork the build, and is suitable for attaching to the build lifecycle. Implementation: org.apache.maven.plugin.source.TestSourceJarNoForkMojo Language: java Bound to phase: package ...
Bound to phase 一项表示该目标默认绑定的生命周期阶段,这里是 package。
插件配置
完成了插件和生命周期的绑定之后,用户还可以配置插件目标的参数,进一步调整插件目标所完成的任务,以满足项目的需求。几乎所有 Maven 插件的目标都有一些可配置的参数,用户可以通过命令行和 POM 配置等方式来配置这些参数。
命令行插件配置
很多插件目标的参数都支持从命令行配置,用户可以在 Maven 命令中使用 -D 参数来配置插件目标的参数。例如,maven-surefire-plugin 提供了一个 maven.test.skip 参数,当其值为 true 时,就会跳过执行测试。所以,在运行命令的时候,如果加上如下 -D 参数就能跳过测试:
$ mvn install -Dmaven.test.skip=true
POM 中插件全局配置
并不是所有的插件参数都适合从命令行配置,有些参数都值从项目创建到项目发布都不会改变或很少改变,这种情况,在 POM 文件中一次性配置就比重复地在命令行输入要方便。
用户可以在声明插件的时候,对插件进行一个全局对配置,即这些配置会作用于所有基于该插件目标的任务。例如,下面的配置为 maven-compile-plugin 指定编译时的 jdk 版本。
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compile-plugin</artifactId> <version>2.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
这样,不管绑定到 compile 阶段的 maven-compile-plugin:compile 任务,还是绑定到 test-compiler 阶段的 maven-compiler-plugin:testComplile 任务,就都会基于 jdk 1.8 版本进行编译。
POM 中插件任务配置
除了为插件配置全局的参数,用户还可以为某个插件任务配置特定的参数。以 maven-antrun-plugin 为例,它有一个目标 run,可以用来在 Maven 中调用 Ant 任务。用户可以将 maven-antrun-plugin:run 绑定到多个生命周期阶段上,再加以不同的配置,就可以让 Maven 在不同的生命周期阶段执行不同的任务,如下所示:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-antrun-plugin</artifactId> <version>2.1</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>
插件仓库
与依赖构件一样,插件构件同样是基于坐标存储在 Maven 仓库中的。在需要的时候,Maven 会从本地仓库寻找插件,如果不存在,则从远程插件仓库中查找。找到插件之后,再下载到本地仓库使用。
但是,Maven 会区别对待依赖到远程仓库与插件的远程仓库。不同于 repositories 及其 repository 子元素,插件的远程仓库使用 pluginRepositories 和 pluginRepository 配置。例如,Maven 内置了如下的插件远程仓库配置:
<pluginRepositories> <pluginRepository> <id>central</id> <name>Central Repository</name> <url>https://repo.maven.apache.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> <releases> <updatePolicy>never</updatePolicy> </releases> </pluginRepository> </pluginRepositories>
除了 pluginRepositories 和 pluginRepository 标签不同之外,其余所有子元素表达的含义,与依赖远程仓库的配置完全一样。而且,默认插件仓库的地址就是中央仓库。
一般来说,中央仓库所包含的插件完全能够满足我们的需要,因此也不需要再配置其他的插件仓库。只有在很少的情况下,项目使用的插件无法在中央仓库找到,或者自己编写了插件,这时可参考上述配置,在 POM 或 settings.xml 中加入其他的插件仓库配置。
从命令行调用插件
我们知道,可以通过 mvn 命令激活生命周期阶段,从而执行那些绑定在生命周期阶段上的插件目标。但 Maven 还支持直接从命令行调用插件目标。Maven 支持这种方式是因为有些任务不适合绑定在生命周期上,例如 maven-dependency-plugin:tree,我们不需要在构建项目但时候去显示依赖树。因此,这类插件目标应该通过如下方式使用:
$ mvn dependency:tree
这条命令等价于下面这条命令:
$ mvn org.apache.maven.plugins:maven-dependency-plugin:2.8:tree
下面这条命令显示地声明了插件的 groupId、artifactId、version 和 goal,但前面但命令更简洁,更容易记忆和使用。为什么 org.apache.maven.plugins:maven-dependency-plugin:2.8 能精简成 dependency 呢?
-
插件默认的 groupId:如果插件是 Maven 的官方插件(即 groupId 为 org.apache.maven.plugins),则可以省略。
-
Maven 引入了插件前缀的概念,在插件仓库中的 org/apache/maven/plugins/maven-metadata.xml,为官方的插件指定了前缀。maven-dependency-plugin 的前缀是 dependency,配置片段如下:
<plugin> <name>Apache Maven Dependency Plugin</name> <prefix>dependency</prefix> <artifactId>maven-dependency-plugin</artifactId> </plugin>
-
如果没有指定插件版本,Maven 会自动解析插件版本。首先,Maven 在超级 POM 中为所有的核心插件设定了版本,这些插件包括 maven-clean-plugin、maven-compiler-plugin、maven-dependency-plugin 等。如果插件不属于核心版本范畴,则会根据仓库元数据计算插件版本的 release 值,作为版本。以 maven-dependency-plugin 为例,它的元数据文件是 org/apache/maven/plugins/maven-dependency-plugin/maven-metadata.xml。