本文整合了 Maven 构建生命周期、插件配置、依赖管理、全局配置 settings.xml、FatJar 打包、测试与覆盖率等全部知识点,是一份系统性的 Maven 参考指南。

maven-生命周期.png
maven-生命周期.xmind

构建生命周期

构建生命周期的基础知识

Maven 基于一个"构建生命周期"的中心概念,也就意味着构建和发布一个特定的工件(也就是工程)的过程已经被清晰地定义了。

有三种内置的生命周期:default,clean 和 site。default 生命周期处理项目部署,clean 生命周期处理项目清理,site 生命周期处理项目的站点(site)文档的创建。

graph TD
    subgraph Clean生命周期
        A1[pre-clean] --> A2[clean]
        A2 --> A3[post-clean]
    end
    
    subgraph Default生命周期
        B1[validate] --> B2[initialize]
        B2 --> B3[generate-sources]
        B3 --> B4[process-sources]
        B4 --> B5[generate-resources]
        B5 --> B6[process-resources]
        B6 --> B7[compile]
        B7 --> B8[process-classes]
        B8 --> B9[generate-test-sources]
        B9 --> B10[process-test-sources]
        B10 --> B11[generate-test-resources]
        B11 --> B12[process-test-resources]
        B12 --> B13[test-compile]
        B13 --> B14[process-test-classes]
        B14 --> B15[test]
        B15 --> B16[prepare-package]
        B16 --> B17[package]
        B17 --> B18[pre-integration-test]
        B18 --> B19[integration-test]
        B19 --> B20[post-integration-test]
        B20 --> B21[verify]
        B21 --> B22[install]
        B22 --> B23[deploy]
    end
    
    subgraph Site生命周期
        C1[pre-site] --> C2[site]
        C2 --> C3[post-site]
        C3 --> C4[site-deploy]
    end

实际上这些 lifecycle 只是在(比如 idea 里)对 phase 归类的时候特别有用,我们平时使用 mvn 命令的时候是无法指定这几个生命周期的。

一个构建生命周期是由多个阶段组成的

上面每个生命周期是由不同的 phase 列表组成的。一个 phase 表示生命周期中的一个执行阶段或步骤。

default 里 phases 的顺序是 validate -> compile -> test -> package -> verify -> install -> deploy

使用命令行

如果我们使用命令:

1
mvn install

实际上到 install 为止所有的 phases 都会执行,所以我们通常只要指定执行某一个生命周期的最后一个 phase 就行了。

如果我们使用命令:

1
mvn clean deploy

maven 会先进行清理,然后再进行部署。如果这是一个多 module 的项目(即有多个子项目),则会遍历每个项目以执行 clean 和 deploy。

如果我们使用命令:

1
mvn clean install -Dmaven.test.skip=true

则执行清理、安装且跳过测试 phase。

每一个构建 phase 是由构建 goal 组成的

phase 下面还可以再细分,细分的单元就是 plugin goal。

plugin goal 其实是一个执行的任务(比 phase 更细粒度)一个 goal 可以和一个或者多个 phase 绑定,甚至在生命周期之外通过直接 invocation 运行。

1
mvn clean dependency:copy-dependencies package

mvn 是一级命令。

clean 和 package 是二级命令,也就是 phase。

dependency 是三级命令,也就是 plugin。

copy-dependencies差不多可以说是参数,也就是 plugin goal。

上面的命令就是执行 clean 及其前面所有的 phases 的所有 goal,单独执行 dependency 插件的 copy-dependencies goal,然后再执行 package 的所有 goal。

一个 goal 被绑定到(bound 是 bind 的过去分词形式)多个 phase,这个 goal 会被执行多遍。

设置你的项目来使用构建生命周期

如何把任务分配到构建 phases 里?

packaging

第一个方法是使用 packaging,它对应的 POM 元素是<packaging>。它的有效值分别是:jar、war、ear 和 pom(pom 也是一个 packaging 值,证明这个项目是 purely meta data)。默认值就是 jar。

packaging 有不同的值,插件的 goal 对各个 phases 的绑定就不同。

jar 的插件绑定如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<phases>
<process-resources>
org.apache.maven.plugins:maven-resources-plugin:2.6:resources
</process-resources>
<compile>
org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
</compile>
<process-test-resources>
org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
</process-test-resources>
<test-compile>
org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
</test-compile>
<test>
org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
</test>
<package>
org.apache.maven.plugins:maven-jar-plugin:2.4:jar
</package>
<install>
org.apache.maven.plugins:maven-install-plugin:2.4:install
</install>
<deploy>
org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy
</deploy>
</phases>

其他 phases 见这个《Plugin Bindings for default Lifecycle Reference》

如何控制构建的语言版本

在 Maven 中控制 Java 编译版本有多种方式,推荐使用以下方式之一:

方式一:使用 properties(推荐)

1
2
3
4
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

方式二:在 compiler plugin 中配置

1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>

方式三:使用 release(Java 9+)

1
2
3
4
5
6
7
8
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>11</release>
</configuration>
</plugin>

注意release 参数会同时设置 sourcetarget,并且会启用跨编译检查,确保代码兼容目标版本。不能同时使用 releasesource/target 参数。

plugins

插件是向 maven 提供 goal 的 artifact,即插件虽然自己包含很多 mojo,但它是以 artifact 的形式向外发布自己的能力(capability)的。例如,compile插件提供两个 goal:compiletestCompile(注意看,没有中间的连字符,所以不是 phase)。

在添加插件的时候要注意,不是只是加入一个 plugin 就万事大吉了,我们需要指定我们想要在构建的时候运行的 goal。

因为 packaging 本身也含有对插件和 goal 的绑定,所以我们要当心混合使用的时候的顺序问题。我们可以使用<execution/>来进行顺序的控制,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugin>
<groupId>org.codehaus.modello</groupId>
<artifactId>modello-maven-plugin</artifactId>
<version>1.8.1</version>
<!-- execution 可以包着 configuration -->
<executions>
<execution>
<configuration>
<models>
<model>src/main/mdo/maven.mdo</model>
</models>
<version>4.0.0</version>
</configuration>
<goals>
<goal>java</goal>
</goals>
</execution>
</executions>
</plugin>

execution 里还可以指定 phases:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<groupId>com.mycompany.example</groupId>
<artifactId>display-maven-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<!-- 默认的 plugin 都是有默认的 phase 属性的,但可以靠配置覆盖这些属性 -->
<phase>process-test-resources</phase>
<goals>
<goal>time</goal>
</goals>
</execution>
</executions>
</plugin>

Maven Wrapper

Maven Wrapper 是 Maven 的一个重要特性,它允许项目在没有预装 Maven 的环境中构建。Wrapper 会下载并使用指定版本的 Maven。

使用 Maven Wrapper

1
2
3
4
5
6
7
# 安装 Maven Wrapper(在项目根目录执行)
mvn wrapper:wrapper

# 使用 Wrapper 构建项目(替代 mvn 命令)
./mvnw clean install
# Windows 系统
mvnw.cmd clean install

Maven Wrapper 的优势

  1. 版本一致性:确保所有开发者和 CI 环境使用相同版本的 Maven
  2. 零安装:新成员只需检出代码即可构建,无需手动安装 Maven
  3. 可移植性:项目自带构建环境,不依赖系统 Maven

配置 Maven 版本

.mvn/wrapper/maven-wrapper.properties 文件中配置:

1
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip

多模块项目中的生命周期执行

在多模块项目中,Maven 会按照模块间的依赖关系顺序执行生命周期。

执行顺序

  1. Maven 首先解析模块间的依赖关系,构建反应堆(Reactor)
  2. 按照依赖顺序执行:被依赖的模块先执行
  3. 如果模块 A 依赖模块 B,则先构建 B,再构建 A
graph LR
    subgraph 多模块项目
        Parent[父POM] --> Child1[common模块]
        Parent --> Child2[service模块]
        Parent --> Child3[web模块]
        
        Child3 -->|依赖| Child2
        Child2 -->|依赖| Child1
    end
    
    subgraph 构建顺序
        Step1[1. common] --> Step2[2. service]
        Step2 --> Step3[3. web]
    end
    
    style Child1 fill:#90EE90
    style Child2 fill:#87CEEB
    style Child3 fill:#DDA0DD

示例

假设有三个模块:commonserviceweb,依赖关系为 web -> service -> common

1
mvn clean install

执行顺序为:common -> service -> web

控制构建范围

1
2
3
4
5
6
7
8
9
10
11
# 只构建特定模块
mvn clean install -pl service

# 构建特定模块及其依赖
mvn clean install -pl service -am

# 构建特定模块及依赖它的模块
mvn clean install -pl service -amd

# 从特定模块开始构建
mvn clean install -rf service

Profile 与生命周期的关系

Profile 允许根据不同的环境或条件使用不同的构建配置。

定义 Profile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<profiles>
<profile>
<id>dev</id>
<properties>
<env>dev</env>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<env>prod</env>
</properties>
</profile>
</profiles>

激活 Profile

1
2
3
4
5
6
7
# 命令行激活
mvn clean install -Pdev

# 通过 settings.xml 激活
<activeProfiles>
<activeProfile>dev</activeProfile>
</activeProfiles>

Profile 中的插件配置

Profile 可以覆盖或扩展插件配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<profile>
<id>prod</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>11</release>
<debug>false</debug>
<optimize>true</optimize>
</configuration>
</plugin>
</plugins>
</build>
</profile>

跳过测试的多种方式

方式一:使用 maven.test.skip

1
mvn clean install -Dmaven.test.skip=true

这种方式会完全跳过测试编译和执行,测试代码不会被编译。

方式二:使用 skipTests

1
mvn clean install -DskipTests

这种方式会编译测试代码,但不执行测试。推荐使用这种方式,因为它可以提前发现测试代码的编译错误。

方式三:使用 surefire 插件配置

1
2
3
4
5
6
7
8
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>

Maven 3.x 与 Maven 4.x 的差异

截至 2024 年,Maven 4.x 仍在开发中,主要的改进方向包括:

性能优化

  • 更快的构建速度
  • 改进的并行构建能力
  • 更好的增量构建支持

依赖管理

  • 更灵活的依赖范围
  • 改进的依赖解析算法

用户界面

  • 更好的错误提示
  • 改进的日志输出

兼容性

  • Maven 4.x 将向后兼容 Maven 3.x 的 POM 文件
  • 但某些废弃的特性将被移除

注意:目前生产环境仍推荐使用 Maven 3.8.x 或 3.9.x 稳定版本。

生命周期参考

clean 生命周期的 phases

phase 含义
pre-clean 在实际的项目清理以前,需要被执行的处理过程(processes)
clean 移走前一个 build 生成的所有文件
post-clean 执行所有需要用来终结项目清理的过程

default 的 phases

phase 含义
validate 验证项目是正确的,且所有必须信息是可用的(实际上就是整个 pom 文件是 valid 的)
initialize 初始化构建状态,即设置 properties 或创建目录。也就是 properties 要在这个阶段被 inject
generate-sources 为任何编译中的inclusion(包含)生成源代码。
process-sources 处理源代码,比如过滤某些值
generate-resources 为打包的时候需要的inclusion(包含)生成资源。也就是中间资源(resources/assets)生成。
process-resources 拷贝和处理资源到目标文件夹里,为打包做准备。也就是静态资源准备完毕
compile 编译项目的源代码
process-classes 后处理编译生成的文件,比如对 Java 类进行字节码增强。比如使用 AOP 框架进行字节码织入
generate-test-sources 为编译中的inclusion(包含)生成测试用源代码
process-test-sources 处理测试源代码,比如过滤某些值
generate-test-resources 为测试创建资源。
process-test-resources 复制和处理资源到测试目标目录
test-compile 编译测试源代码到测试目标目录
process-test-classes 对测试编译的结果进行后处理,比如进行字节码增强,需要 Maven 2.0.5 和以上版本。
test 用测试框架中合适的单元运行测试,这些测试不能要求类被打包和部署
prepare-package 进行任何实际打包前必须的准备操作
package 把编译过的代码装进一个可发布格式,比如 JAR
pre-integration-test 进行集成测试前需要的活动,比如设置一个合适的环境
integration-test 处理和部署一个包到集成测试环境,运行集成测试
post-integration-test 执行集成测试后需要执行的活动,比如清理环境
verify 跑一些校验,来验证包是有效的,而且满足质量标准的
install 把当前的包安装到本地仓库,这样可以作为其他项目的本地依赖
deploy 在集成或者发布环境中执行,把包拷贝到一个远程的仓库,给其他开发者和项目共享

site 的 phases

phase 含义
pre-site 执行需要在站点生成(generation)之前执行的流程
site 生成项目的站点文档
post-site 执行需要被用来终结站点生成的操作,来为站点部署做准备
site-deploy 部署站点文档到特定的 web server 里(而不是仓库里)

插件配置

本章主要参考了《Guide to Configuring Plug-ins》

maven 实际上有两种插件:

  • 构建插件,需要在<build/>元素里配置。如<build><pluginManagement/></build>,当然也有<build><plugins/></build>
  • 报告插件,会在"site generation"里被执行,应该在<reporting/>里配置。如<reporting><plugins/></reporting>

要引用插件至少要有三个元素:groupIdartifactIdversion

mojo 是什么

根据《What is MOJO in Maven?》,mojo 是 Maven plain Old Java Object 的意思。实际上是可执行的 goal。

通用配置

一个插件通常包含一个以上的mojo,当一个 mojo 被映射到 goal 的时候,则包含多个 mojo(即一个插件可能有多个 goal)。maven 通过 元素来配置 maven 插件, 的子元素,就会映射到 mojo 的字段,或者 setter 里。

假设有一个mojo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @goal query
*/
public class MyQueryMojo
extends AbstractMojo
{
/**
* @parameter expression="${query.url}"
*/
private String url;

/**
* @parameter default-value="60"
*/
private int timeout;

/**
* @parameter
*/
private String[] options;

public void execute()
throws MojoExecutionException
{
...
}
}

生成的 xml 就如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project>
...
<build>
<plugins>
<plugin>
<artifactId>maven-myquery-plugin</artifactId>
<version>1.0</version>
<configuration>
<url>http://www.foobar.com/query</url>
<timeout>10</timeout>
<options>
<option>one</option>
<option>two</option>
<option>three</option>
</options>
</configuration>
</plugin>
</plugins>
</build>
...
</project>

可以看到<configuration/>确实是 schemaless 的。

而且这些 field 上的注释可以注明缺省值,以及与命令行参数一起使用时的表达式:

1
2
# 调用 myquery 插件 query goal,-D 引出命令行参数,在底层可以通过 System.getProperties() 取值,相当于系统变量(而不是环境变量)的一部分
mvn myquery:query -Dquery.url=http://maven.apache.org

查看帮助

通常插件都带有一个help的 goal,通常可以用以下的方法查看(自行替换插件名称):

1
mvn javadoc:help -Ddetail -Dgoal=javadoc

配置参数

普通类型

基本类型的映射,就使用字面量配置<configuration/>元素:

1
2
3
4
5
6
7
8
<configuration>
<myString>a string</myString>
<myBoolean>true</myBoolean>
<myInteger>10</myInteger>
<myDouble>1.0</myDouble>
<myFile>c:\temp</myFile>
<myURL>http://maven.apache.org</myURL>
</configuration>

复杂类型

如果有复杂(complex not compoud)类型映射的需要:

1
2
3
4
5
6
<configuration>
<person>
<firstName>Jason</firstName>
<lastName>van Zyl</lastName>
</person>
</configuration>

maven 会以大写首字母的方式,在 mojo 本身存在的包里寻找 person 类,否则,就要指定包名:

1
2
3
4
5
6
<configuration>
<person implementation="com.mycompany.mojo.query.SuperPerson">
<firstName>Jason</firstName>
<lastName>van Zyl</lastName>
</person>
</configuration>

列表类型

同数组不同,列表类型在 maven 的xml 语法里不是强类型的(换言之,数组是):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyAnimalMojo
extends AbstractMojo
{
/**
* @parameter
*/
private List animals;

public void execute()
throws MojoExecutionException
{
...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<project>
...
<build>
<plugins>
<plugin>
<artifactId>maven-myanimal-plugin</artifactId>
<version>1.0</version>
<configuration>
<animals>
<animal>cat</animal>
<animal>dog</animal>
<animal>aardvark</animal>
</animals>
</configuration>
</plugin>
</plugins>
</build>
...
</project>

其映射规则是:

  • If the XML element contains an implementation hint attribute, that is used
  • If the XML tag contains a ., try that as a fully qualified class name
  • Try the XML tag (with capitalized first letter) as a class in the same package as the mojo/object being configured
  • If the element has no children, assume its type is String. Otherwise, the configuration will fail.

map 类型

1
2
3
4
5
6
/**
* My Map.
*
* @parameter
*/
private Map myMap;
1
2
3
4
5
6
7
8
...
<configuration>
<myMap>
<key1>value1</key1>
<key2>value2</key2>
</myMap>
</configuration>
...

Properties 类型

和 map 表达嵌套的方式恰好又不一样了,更加工整:

1
2
3
4
5
6
/**
* My Properties.
*
* @parameter
*/
private Properties myProperties;
1
2
3
4
5
6
7
8
9
10
11
12
<configuration>
<myProperties>
<property>
<name>propertyName1</name>
<value>propertyValue1</value>
<property>
<property>
<name>propertyName2</name>
<value>propertyValue2</value>
<property>
</myProperties>
</configuration>

配置构建插件

使用<executions>标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<project>
...
<build>
<plugins>
<plugin>
<artifactId>maven-myquery-plugin</artifactId>
<version>1.0</version>
<executions>
<!-- execution 实际上是插件的执行配置 -->
<execution>
<!-- 执行的 id -->
<id>execution1</id>
<!--这个执行所属的阶段-->
<phase>test</phase>
<configuration>
<!--配置内部的属性-->
<url>http://www.foo.com/query</url>
<timeout>10</timeout>
<options>
<option>one</option>
<option>two</option>
<option>three</option>
</options>
</configuration>
<!--这一个 execution 需要执行的 goal 是什么 -->
<goals>
<goal>query</goal>
</goals>
</execution>
<execution>
<id>execution2</id>
<configuration>
<url>http://www.bar.com/query</url>
<timeout>15</timeout>
<options>
<option>four</option>
<option>five</option>
<option>six</option>
</options>
</configuration>
<goals>
<goal>query</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
</project>

注意,在一个 POM 的一个插件里,execution id 必须是唯一的。

一个 plugin 在多个 phase 多次被执行的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<project>
...
<build>
<plugins>
<plugin>
...
<executions>
<execution>
<id>execution1</id>
<phase>test</phase>
...
</execution>
<execution>
<id>execution2</id>
<phase>install</phase>
<configuration>
<url>http://www.bar.com/query</url>
<timeout>15</timeout>
<options>
<option>four</option>
<option>five</option>
<option>six</option>
</options>
</configuration>
<goals>
<goal>query</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
</project>

下面这个 mojo,用注释标明了这个 plugin/goal 的默认 phase:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @goal query
* @phase package
*/
public class MyBindedQueryMojo
extends AbstractMojo
{
/**
* @parameter expression="${query.url}"
*/
private String url;

/**
* @parameter default-value="60"
*/
private int timeout;

/**
* @parameter
*/
private String[] options;

public void execute()
throws MojoExecutionException
{
...
}
}

我们可以使用 xml 配置来覆盖初始配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<project>
...
<build>
<plugins>
<plugin>
<artifactId>maven-myquery-plugin</artifactId>
<version>1.0</version>
<executions>
<execution>
<id>execution1</id>
<phase>install</phase>
<configuration>
<url>http://www.bar.com/query</url>
<timeout>15</timeout>
<options>
<option>four</option>
<option>five</option>
<option>six</option>
</options>
</configuration>
<goals>
<goal>query</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
</project>

曾经,maven 插件的如果处于内部,则无法被命令行 invocation 调用,因为它必须到指定生命周期 phase 才可以被使用。但自 Maven 3.3.1 以后,我们可以直接这样做:

1
2
# execution id 终于派上用场了
mvn myqyeryplugin:queryMojo@execution1

使用<dependencies>标签

插件自己也有自己的默认 dependency,我们可以在插件内部自己使用<dependencies>来更换它的依赖。如 Maven Antrun Plugin version 1.2 使用的 Ant version 1.6.5,我们可以这样更新依赖(兼容性自己保证):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<project>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.2</version>
...
<dependencies>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant-launcher</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
...
</project>

在构建插件里使用<inherited>标签

默认的插件配置是会被传播(propagated)到子 pom 里的,所以可以显式地设置<inherited>为 false 来打破这种属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<project>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.2</version>
<!-- 不使用这个 tag 的时候,这个值默认就是 false -->
<inherited>false</inherited>
...
</plugin>
</plugins>
</build>
...
</project>

配置 reporting 插件

使用 <reporting> Tag VS <build> Tag

<build> 标签里也可以配置 reporting 插件,但大部分情况下<reporting>里的配置都优先被使用。具体细则见 maven 原文档

使用<reportSets>标签

如果我们想要选择性地生成报告,我们可以使用<reportSets>标签,只有被它包括的报告,才被选中生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<project>
...
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>2.1.2</version>
<reportSets>
<reportSet>
<reports>
<report>project-team</report>
</reports>
</reportSet>
</reportSets>
</plugin>
</plugins>
</reporting>
...
</project>

推而广之,一份报告都不生成的时候,我们可以这样做:

1
2
3
4
5
<reportSets>
<reportSet>
<reports/>
</reportSet>
</reportSets>

在报告插件中使用<inherited>标签

同构建插件差不多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
...
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>2.1.2</version>
<inherited>false</inherited>
</plugin>
</plugins>
</reporting>
...
</project>

依赖管理

Maven 的配置和依赖是单根继承的

Maven 的模块继承是无法进行多继承的,只能使用单根继承。

dependencies 与 dependencyManagement 的区别

dependencies 即使在子项目中不写该依赖项,那么子项目仍然会从父项目中继承该依赖项(全部继承)

dependencyManagement 里只是声明依赖和它们的版本,并不实现引入,因此子项目需要显示的声明需要用的依赖。如果不在子项目中声明依赖,是不会从父项目中继承下来的;只有在子项目中写了该依赖项,并且没有指定具体版本,才会从父项目中继承该项,并且 version 和 scope 都读取自父 pom; 另外如果子项目中指定了版本号,那么会使用子项目中指定的jar版本。

有效 pom

1
2
3
mvn help:effective-pom > effective-pom.xml
# 获取某个指定 artifact 的依赖树
mvn dependency:tree -Dverbose -Dincludes=com.fasterxml.jackson.core:jackson-core

下载依赖中的源码和文档

在 maven 里,resolve 有特别的含义:仲裁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 只下载源码
mvn dependency:sources

# 静默下载源码
mvn dependency:sources -Dsilent=true

# 只下载文档
mvn dependency:resolve -Dclassifier=javadoc
# 一起下载
mvn dependency:sources dependency:resolve -Dclassifier=javadoc

# 主动获取源码,这样做可以帮助其他构件工具使用 mavenLocal() 仓库
mvn dependency:get -Dartifact=GROUP_ID:ARTIFACT_ID:VERSION:jar:sources
mvn dependency:get -Dartifact=org.springframework:spring-core:5.1.2.RELEASE:jar:sources

在 .m2 中这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<settings>
<!-- ... other settings omitted ... -->
<profiles>
<profile>
<id>downloadSources</id>
<properties>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</properties>
</profile>
</profiles>

<activeProfiles>
<activeProfile>downloadSources</activeProfile>
</activeProfiles>
</settings>

在项目中这样主动配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<goals>
<goal>sources</goal>
<goal>resolve</goal>
</goals>
<configuration>
<classifier>javadoc</classifier>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

附赠一个批量构建脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash

function traverseAllProjects () {
for f in *; do
if [ -d "$f" ]; then
echo "coming into a folder $f"
cd $f
echo "current path:"
git pull
pwd
POM_FILE=pom.xml
if test -f "$POM_FILE"; then
echo "$POM_FILE exists."
# clean build
time mvn clean install -Dmaven.test.skip=true
# resolve dependencies' javadocs and sources
time mvn dependency:sources dependency:resolve -Dclassifier=javadoc\n
# complete build
time mvn clean install -Dmaven.test.skip=true
fi
GRADLE_FILE=build.gradle
if test -f "$GRADLE_FILE"; then
echo "$GRADLE_FILE exists."
# clean build
time ./gradlew clean build -x test
# 目前还没有简单的在命令行里下载源代码的方法
fi
echo "coming out a folder $f"
cd ..
echo "current path:"
pwd
fi
done
}

time traverseAllProjects

在 idea中这样配置 Preference > Build, Execution, Deployment > Build Tools > Maven > importing

BOM(Bill of Material)

BOM定义

BOM(Bill of Material),物料清单。将某一个领域相应的jar统一称之为XXX-BOM予以管理(例如inf-bom、spring-framework-bom),并不是Maven的规定,只是一种约定俗成(convention over configuration)。Gradle 也采用了这样的思路

为什么要使用bom

(1)避免开发同学需要关心各个API的版本关系:BOM这种方式在 dependencyManagement 指定了各个依赖的版本,并不会使得这些依赖并被真正引入(仅做版本管理使用)。在dependency中还要按需引入、但省去了需要制定version的麻烦。

(2)便于历史版本API的收归。

(3)可以说逐渐使用 bom 来统一管理某个团队(领域)的 API 版本已成为一种趋势。

scope 的含义

scope元素的作用:控制 dependency 元素的使用范围。通俗的讲,就是控制 Jar 包在哪些范围被加载和使用。

compile(默认)

含义:compile 是默认值,如果没有指定 scope 值,该元素的默认值为 compile。被依赖项目需要参与到当前项目的编译,测试,打包,运行等阶段。打包的时候通常会包含被依赖项目。

provided

含义:被依赖项目理论上可以参与编译、测试、运行等阶段,相当于compile,但是再打包阶段做了exclude的动作。
适用场景:例如, 如果我们在开发一个web 应用,在编译时我们需要依赖 servlet-api.jar,但是在运行时我们不需要该 jar 包,因为这个 jar 包已由应用服务器提供,此时我们需要使用 provided 进行范围修饰。

runtime

含义:表示被依赖项目无需参与项目的编译,但是会参与到项目的测试和运行。与compile相比,被依赖项目无需参与项目的编译。
适用场景:例如,在编译的时候我们不需要 JDBC API 的 jar 包,而在运行的时候我们才需要 JDBC 驱动包。

test

含义: 表示被依赖项目仅仅参与测试相关的工作,包括测试代码的编译,执行。
适用场景:例如,Junit 测试。

system

含义:system 元素与 provided 元素类似,但是被依赖项不会从 maven 仓库中查找,而是从本地系统中获取,systemPath 元素用于制定本地系统中 jar 文件的路径。例如:

1
2
3
4
5
6
7
<dependency>
<groupId>org.open</groupId>
<artifactId>open-core</artifactId>
<version>1.5</version>
<scope>system</scope>
<systemPath>${basedir}/WebContent/WEB-INF/lib/open-core.jar</systemPath>
</dependency>

import

它只使用在中,表示从其它的pom中导入dependency的配置,例如 (B项目导入A项目中的包配置):

想必大家在做SpringBoot应用的时候,都会有如下代码:

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.3.3.RELEASE</version>
</parent>

继承一个父模块,然后再引入相应的依赖。
假如说,我不想继承,或者我想继承多个,怎么做?

我们知道Maven的继承和Java的继承一样,是无法实现多重继承的,如果10个、20个甚至更多模块继承自同一个模块,那么按照我们之前的做法,这个父模块的dependencyManagement会包含大量的依赖。如果你想把这些依赖分类以更清晰的管理,那就不可能了,import scope依赖能解决这个问题。你可以把 dependencyManagement 放到单独的专门用来管理依赖的pom中,然后在需要使用依赖的模块中通过import scope依赖,就可以引入 dependencyManagement。例如可以写这样一个用于依赖管理的pom:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.test.sample</groupId>
<artifactId>base-parent1</artifactId>
<packaging>pom</packaging>
<version>1.0.0-SNAPSHOT</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
<version>4.8.2</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
<version>1.2.16</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

然后我就可以通过非继承的方式来引入这段依赖管理配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test.sample</groupId>
<artifactid>base-parent1</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependency>
<groupId>junit</groupId>
<artifactid>junit</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactid>log4j</artifactId>
</dependency>

注意:import scope只能用在 dependencyManagement 里面。这么多的 scope 里面,import 也因此是最危险的,因为 import 会把依赖直接展开,而不是用间接传递的方式在新应用中体现,会覆盖 parent 和 dependency(因为寻根路径最短,链接器会最先被链接上),而且无法被 exclude 排除

这样,父模块的pom就会非常干净,由专门的packaging为pom来管理依赖,也契合的面向对象设计中的单一职责原则。此外,我们还能够创建多个这样的依赖管理pom,以更细化的方式管理依赖。这种做法与面向对象设计中使用组合而非继承也有点相似的味道。

那么,如何用这个方法来解决SpringBoot的那个继承问题呢?

配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.3.3.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

这样配置的话,自己的项目里面就不需要继承SpringBoot的module了,而可以继承自己项目的module了。

scope的依赖传递

A–>B–>C。当前项目为A,A依赖于B,B依赖于C。知道B在A项目中的scope,那么怎么知道C在A中的scope呢?答案是:
当C是test或者provided时,C直接被丢弃,A不依赖C;
否则A依赖C,C的scope继承于B的scope。

graph LR
    A[项目A] -->|依赖 scope: compile| B[项目B]
    B -->|依赖 scope: compile| C[项目C]
    
    A -->|依赖 scope: test| T[项目T]
    T -->|依赖 scope: compile| U[项目U]
    
    style C fill:#90EE90
    style U fill:#FFB6C1
    
    classDef green fill:#90EE90,stroke:#333,stroke-width:2px
    classDef red fill:#FFB6C1,stroke:#333,stroke-width:2px
    class C green
    class U red

依赖传递规则

B的scope C的scope C在A中的结果
compile compile compile
compile test ✗ 被丢弃
compile provided ✗ 被丢弃
test compile ✗ 被丢弃
provided compile provided
runtime compile runtime

依赖矩阵表格见:《Introduction to the Dependency Mechanism》

scope 与 optional 的区别

maven的 scope 决定依赖的包是否加入本工程的classpath下。- 某些 scope 连本项目的 classpath 都会被影响。使用本项目的项目无论如何都不能绕过 scope 的影响,scope 才是最彻底的对传播的隔离(比如 provided)。

optional仅限制依赖包的传递性,不影响依赖包的classpath。- 不影响本项目生成的 jar,影响使用本项目的项目。

scope 与 optional 都可以用重新声明依赖的方式来引入缺失依赖。

比如一个工程中

A->B, B->C(scope:compile, optional:true),B的编译/运行/测试classpath都有C,A中的编译/运行/测试classpath都不存在C(尽管C的scope声明为compile),A调用B哪些依赖C的方法就会出错。

A->B, B->C(scope:provided), B的编译/测试classpath有C,A中的编译/运行/测试classpath都不存在C,但是A使用B(需要依赖C)的接口时就会出现找不到C的错误,此时,要么是手动加入C的依赖,即A->C,否则需要容器提供C的依赖包到运行时classpath。

对于纯粹作为 lib 来用的 jar,rovided over optional。因为出了 test 这个 phase,连 jar 都不能独立 run 起来。optional 本身是一个可以自己在各种 phase run,但被依赖的时候则会去除打包配置,依然会影响 classpath。

optional 是大项目无法被切割成小的子模块的无奈选择,如果项目要使用被依赖模块的可选功能,必须显式地再声明一遍可选依赖,否则会产生调用出错。optional 阻断了传递依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
...
<dependencies>
<!-- declare the dependency to be set as optional -->
<dependency>
<groupId>sample.ProjectA</groupId>
<artifactId>Project-A</artifactId>
<version>1.0</version>
<scope>compile</scope>
<optional>true</optional> <!-- value will be true or false only -->
</dependency>
</dependencies>
</project>

Project-A -> Project-B Project-X -> Project-A

A 的类路径里有 B,而 X 的类路径里无 B。

依赖排除与 debug 技巧

  • 在子工程里显式地指定某个依赖版本看是否能够消除错误。
  • 使用 ide 的依赖分析工具,如 mvn dependency 插件(这个分析工具只是运行时分析,有误导性)或者 idea 的 dependency analyzer。
  • 显式地消除依赖:
1
2
3
4
5
6
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</exclusion>
</exclusions>

maven-enforcer-plugin 解决包冲突

执行时机

生命周期validate环节。

dependencyConvergence执行逻辑

通过访问maven dependency tree生成的依赖树,存入map中,key是groupid和artifactId组合,value是依赖对象list,通过判断每个list里的版本号是否相同来判断所有依赖是否为同一个版本。

配置实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M3</version>
<executions>
<execution>
<id>enforce</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<bannedDependencies>
<excludes>
<exclude>com.alibaba:fastjson:(,1.2.60)</exclude>
<exclude>org.hibernate:hibernate-validator</exclude>
</excludes>
</bannedDependencies>
</rules>
<fail>true</fail>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

快速检验

在自己分支开发完成后,执行mvn validate命令即可查看冲突信息

移除冲突jar

登陆线上机器确认冲突jar的版本,移除不是对应版本的jar,方法:Dependency Analyzer 中搜索冲突jar名,右键选择移除即可自动生成exclude内容。

versions-maven-plugin 版本管理

参考:《Use the Latest Version of a Dependency in Maven》

Maven2 also provided two special metaversion values to achieve the
result: LATEST and RELEASE.

但这两个值已经过期了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.7</version>
<configuration>
<excludes>
<exclude>org.apache.commons:commons-collections4</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
1
2
3
4
5
6
7
8
9
10
# 校验当前的项目有哪些依赖有 newer version
mvn versions:display-dependency-updates

# 会改变 pom 的版本,产生一个备份:pom.xml.versionsBackup,会把 1.1.1-SNAPSHOT,转成 1.1.1
mvn versions:use-releases

# 使用下一个版本的 release
mvn versions:use-next-releases
# 使用最新的 release
mvn versions:use-latest-releases
1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.7</version>
<configuration>
<rulesUri>http://www.mycompany.com/maven-version-rules.xml</rulesUri>
<!-- <rulesUri>file:///home/andrea/maven-version-rules.xml</rulesUri> -->
</configuration>
</plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<ruleset comparisonMethod="maven"
xmlns="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0
http://mojo.codehaus.org/versions-maven-plugin/xsd/rule-2.0.0.xsd">
<ignoreVersions>
<ignoreVersion type="regex">.*-beta</ignoreVersion>
</ignoreVersions>
</ruleset>

<ruleset comparisonMethod="maven"
xmlns="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0
http://mojo.codehaus.org/versions-maven-plugin/xsd/rule-2.0.0.xsd">
<rules>
<rule groupId="com.mycompany.maven" comparisonMethod="maven">
<ignoreVersions>
<ignoreVersion type="regex">.*-RELEASE</ignoreVersion>
<ignoreVersion>2.1.0</ignoreVersion>
</ignoreVersions>
</rule>
</rules>
</ruleset>

全局配置 settings.xml

文件结构与顶层元素

settings.xml 对于 maven 来说相当于全局性的配置,用于所有的项目。在 Maven2 中存在两个 settings.xml,一个位于 maven2 的安装目录 conf 下面,作为全局性配置。对于团队设置,保持一致的定义是关键,所以 maven2/conf 下面的 settings.xml 就作为团队共同的配置文件。而对于每个成员,如果有特殊的需求,则可以在 ~/.m2/settings.xml 中进行设置。

settings.xml 基本结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository/>
<interactiveMode/>
<usePluginRegistry/>
<offline/>
<pluginGroups/>
<servers/>
<mirrors/>
<proxies/>
<profiles/>
<activeProfiles/>
</settings>

LocalRepository

作用:该值表示构建系统本地仓库的路径。其默认值:~/.m2/repository

1
<localRepository>${user.home}/.m2/repository</localRepository>

InteractiveMode

作用:表示maven是否需要和用户交互以获得输入。如果maven需要和用户交互以获得输入,则设置成true,反之则应为false。默认为true。

1
<interactiveMode>true</interactiveMode>

Offline

作用:表示maven是否需要在离线模式下运行。如果构建系统需要在离线模式下运行,则为true,默认为false。当由于网络设置原因或者安全因素,构建服务器不能连接远程仓库的时候,该配置就十分有用。

1
<offline>false</offline>

PluginGroups

作用:当插件的组织id(groupId)没有显式提供时,供搜寻插件组织Id(groupId)的列表。该元素包含一个pluginGroup元素列表,每个子元素包含了一个组织Id(groupId)。当我们使用某个插件,并且没有在命令行为其提供组织Id(groupId)的时候,Maven就会使用该列表。默认情况下该列表包含了 org.apache.maven.pluginsorg.codehaus.mojo

1
2
3
4
<pluginGroups>
<!--plugin的组织Id(groupId) -->
<pluginGroup>org.codehaus.mojo</pluginGroup>
</pluginGroups>

Servers

作用:一般,仓库的下载和部署是在pom.xml文件中的repositories和distributionManagement元素中定义的。然而,一般类似用户名、密码(有些仓库访问是需要安全认证的)等信息不应该在pom.xml文件中配置,这些信息可以配置在 settings.xml 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<servers>
<server>
<!--这是server的id(注意不是用户登陆的id),该id与distributionManagement中repository元素的id相匹配。 -->
<id>server001</id>
<!--鉴权用户名。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。 -->
<username>my_login</username>
<!--鉴权密码 。鉴权用户名和鉴权密码表示服务器认证所需要的登录名和密码。密码加密功能已被添加到2.1.0 +。详情请访问密码加密页面 -->
<password>my_password</password>
<!--鉴权时使用的私钥位置。和前两个元素类似,私钥位置和私钥密码指定了一个私钥的路径(默认是${user.home}/.ssh/id_dsa)以及如果需要的话,一个密语。将来passphrase和password元素可能会被提取到外部,但目前它们必须在settings.xml文件以纯文本的形式声明。 -->
<privateKey>${usr.home}/.ssh/id_dsa</privateKey>
<!--鉴权时使用的私钥密码。 -->
<passphrase>some_passphrase</passphrase>
<!--文件被创建时的权限。如果在部署的时候会创建一个仓库文件或者目录,这时候就可以使用权限(permission)。这两个元素合法的值是一个三位数字,其对应了unix文件系统的权限,如664,或者775。 -->
<filePermissions>664</filePermissions>
<!--目录被创建时的权限。 -->
<directoryPermissions>775</directoryPermissions>
<!--传输层额外的配置项 -->
<configuration></configuration>
</server>
</servers>

Mirrors

作用:为仓库列表配置的下载镜像列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<settings>
...
<mirrors>
<mirror>
<!-- 该镜像的唯一标识符。id用来区分不同的mirror元素。 -->
<id>planetmirror.com</id>
<!-- 镜像名称 -->
<name>PlanetMirror Australia</name>
<!-- 该镜像的URL。构建系统会优先考虑使用该URL,而非使用默认的服务器URL。 -->
<url>http://downloads.planetmirror.com/pub/maven2</url>
<!-- 被镜像的服务器的id。例如,如果我们要设置了一个Maven中央仓库(http://repo.maven.apache.org/maven2/)的镜像,就需要将该元素设置成central。这必须和中央仓库的id central完全一致。 -->
<mirrorOf>central</mirrorOf>
<!-- 是否阻塞 http 访问强制 https -->
<blocked>false</blocked>
</mirror>
</mirrors>
...
</settings>

两个例子。现在的主流仓库都是https了,如果使用http可能遇到 301 报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>https://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>

<mirrors>
<mirror>
<id>repo1</id>
<mirrorOf>central</mirrorOf>
<name>maven repo1</name>
<url>https://repo1.maven.org/maven2/</url>
</mirror>
</mirrors>

Proxies

作用:用来配置不同的代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
...
<proxies>
<!--代理元素包含配置代理时需要的信息 -->
<proxy>
<!--代理的唯一定义符,用来区分不同的代理元素。 -->
<id>myproxy</id>
<!--该代理是否是激活的那个。true则激活代理。当我们声明了一组代理,而某个时候只需要激活一个代理的时候,该元素就可以派上用处。 -->
<active>true</active>
<!--代理的协议。 协议://主机名:端口,分隔成离散的元素以方便配置。 -->
<protocol>http</protocol>
<!--代理的主机名。协议://主机名:端口,分隔成离散的元素以方便配置。 -->
<host>proxy.somewhere.com</host>
<!--代理的端口。协议://主机名:端口,分隔成离散的元素以方便配置。 -->
<port>8080</port>
<!--代理的用户名,用户名和密码表示代理服务器认证的登录名和密码。 -->
<username>proxyuser</username>
<!--代理的密码,用户名和密码表示代理服务器认证的登录名和密码。 -->
<password>somepassword</password>
<!--不该被代理的主机名列表。该列表的分隔符由代理服务器指定;例子中使用了竖线分隔符,使用逗号分隔也很常见。 -->
<nonProxyHosts>*.google.com|ibiblio.org|192.168.58.*|10.*|mirrors.xxx.com</nonProxyHosts>
<!-- 只有高版本的maven可以理解这个配置 -->
<sslHostConfig>
<!--指定是否信任所有证书-->
<all>true</all>
<!--指定使用的SSL协议版本-->
<sslProtocol>all</sslProtocol>
<!--指定是否启用SSL-->
<sslEnabled>true</sslEnabled>
<!--指定支持的SSL协议版本列表-->
<sslProtocols>TLSv1.2</sslProtocols>
<!--指定是否忽略证书验证-->
<ignoreCertificates>true</ignoreCertificates>
<!-- 指定是否信任自签名证书-->
<trustSelfSigned>true</trustSelfSigned>
<!-- 指定是否允许所有证书 -->
<allowAllCerts>true</allowAllCerts>
</sslHostConfig>
</proxy>
</proxies>
...
</settings>

Profiles(settings.xml 版)

作用:根据环境参数来调整构建配置的列表。
settings.xml中的profile元素是pom.xml中profile元素的裁剪版本。
它包含了id、activation、repositories、pluginRepositories和 properties元素。这里的profile元素只包含这五个子元素是因为这里只关心构建系统这个整体(这正是settings.xml文件的角色定位),而非单独的项目对象模型设置。如果一个settings.xml中的profile被激活,它的值会覆盖任何其它定义在pom.xml中带有相同id的profile。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
...
<profiles>
<profile>
<!-- profile的唯一标识 -->
<id>test</id>
<!-- 自动触发profile的条件逻辑 -->
<activation />
<!-- 扩展属性列表 -->
<properties />
<!-- 远程仓库列表 -->
<repositories />
<!-- 插件仓库列表 -->
<pluginRepositories />
</profile>
</profiles>
...
</settings>

快照与发布

我们考虑这种问题要分清楚:

  1. 我们是使用者还是开发者?这决定了我们使用 repository/pluginRepository,还是 distributionManagement。
  2. 我们如果是使用者,我们要分清我们是使用第一方还是第三方,然后我们要分清我们要使用快照还是发布。
  3. 当我们选择快照和发布的时候,我们还要分清 updatePolicy。

对于 Maven 来说,snapshot 和 release 仓库的设置有以下区别:

  • 仓库的用途不同:
    • snapshot 仓库用于存储开发过程中的不稳定版本,这些版本的构件可能会频繁更新。
    • release 仓库用于存储稳定的发布版本,这些版本的构件不会再进行修改。
  • 版本号的约定不同:
    • snapshot 版本的版本号以 “-SNAPSHOT” 结尾,如 “1.0-SNAPSHOT”。
    • release 版本的版本号不包含 “-SNAPSHOT” 后缀,如 “1.0”。
  • 更新策略不同:
    • 对于 snapshot 版本,Maven 每次构建时都会自动检查远程仓库是否有更新,并下载最新的构件。
    • 对于 release 版本,Maven 默认不会检查更新,而是直接使用本地仓库中的构件。
  • 部署策略不同:
    • snapshot 版本通常会频繁部署到 snapshot 仓库中,以供其他开发者使用。
    • release 版本通常只会在稳定后部署一次到 release 仓库中,不再进行修改。

这种 snapshot 和 release 的区分确实是一种约定高于配置的设计。Maven 通过版本号的约定来区分构件的稳定性,并根据约定自动选择适当的仓库和更新策略。这种约定简化了配置,提高了构建的可重复性和可靠性。

其他构建工具也有类似的约定:

  • Gradle 使用 “-SNAPSHOT” 后缀来标识 snapshot 版本,并支持配置不同的 snapshot 和 release 仓库。
  • Ivy 使用 “latest.integration” 和 “latest.release” 来区分 snapshot 和 release 版本,并支持配置不同的仓库。
  • sbt (Scala 构建工具)使用 “-SNAPSHOT” 后缀来标识 snapshot 版本,并支持配置不同的 snapshot 和 release 仓库。

一个完整的远程仓库,需要配置至少如下四类 repo 来管理 jar:

  • release
  • snapshot
  • thirdParty-release
  • thirdParty-snapshot

其中

  • 标准的用法里,首先从仓库名称标识出仓库的性质,然后使用属性进一步区分快照和发布:
    • 对于 release 仓库:releases enabled 为 true,snapshots enabled 为 false;
    • 对于 snapshot 仓库:releases enabled 为 false,snapshots enabled 为 true。
    • 对于 maven central 或者它的镜像,选项都是 true。
  • 后两者其实并不是特别的类型,而是在 id 的名字上有所区别。如设计 <id>thirdparty</id>或者<id>thirdparty-snapshots</id><repository>。这也就意味着还可以有二方的仓库和四方的仓库,只要你能够从 id 里区分开这些仓库

这四种仓库在 Maven 中有不同的用途和约定:

  • release 仓库: 用于存储和管理项目自己的稳定发布版本的构件。这些构件的版本号不包含 “-SNAPSHOT” 后缀,表示它们是经过测试和验证的稳定版本。
  • snapshot 仓库: 用于存储和管理项目自己的开发版本的构件。这些构件的版本号以 “-SNAPSHOT” 结尾,表示它们是不稳定的开发版本。
  • thirdParty-release 仓库: 用于存储和管理第三方的稳定发布版本的构件。将第三方的 release 构件单独存储,可以与项目自己的 release 构件分开管理。
  • thirdParty-snapshot 仓库: 用于存储和管理第三方的开发版本的构件。将第三方的 snapshot 构件单独存储,可以与项目自己的 snapshot 构件分开管理。

两个具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<repositories>
<repository>
<id>some-company-releases</id>
<name>Repository for releases artifacts</name>
<url>http://pixel.some-company.com/repository/group-releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</releases>
</repository>
<repository>
<id>some-company-snapshots</id>
<name>Repository for snapshots artifacts</name>
<url>http://pixel.some-company.com/repository/group-snapshots</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>some-company-releases-plugin</id>
<name>Repository for plugin releases artifacts</name>
<url>http://pixel.some-company.com/repository/group-releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</releases>
</pluginRepository>
<pluginRepository>
<id>some-company-snapshots-plugin</id>
<name>Repository for plugin snapshots artifacts</name>
<url>http://pixel.some-company.com/repository/group-snapshots</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>

Activation

作用:自动触发profile的条件逻辑。
如pom.xml中的profile一样,profile的作用在于它能够在某些特定的环境中自动使用某些特定的值;这些环境通过activation元素指定。
activation元素并不是激活profile的唯一方式。settings.xml文件中的activeProfile元素可以包含profile的id。profile也可以通过在命令行,使用-P标记和逗号分隔的列表来显式的激活(如,-P test)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<activation>
<!--profile默认是否激活的标识 -->
<activeByDefault>false</activeByDefault>
<!--当匹配的jdk被检测到,profile被激活。例如,1.4激活JDK1.4,1.4.0_2,而!1.4激活所有版本不是以1.4开头的JDK。 -->
<jdk>1.5</jdk>
<!--当匹配的操作系统属性被检测到,profile被激活。os元素可以定义一些操作系统相关的属性。 -->
<os>
<!--激活profile的操作系统的名字 -->
<name>Windows XP</name>
<!--激活profile的操作系统所属家族(如 'windows') -->
<family>Windows</family>
<!--激活profile的操作系统体系结构 -->
<arch>x86</arch>
<!--激活profile的操作系统版本 -->
<version>5.1.2600</version>
</os>
<!--如果Maven检测到某一个属性(其值可以在POM中通过${name}引用),其拥有对应的name = 值,Profile就会被激活。如果值字段是空的,那么存在属性名称字段就会激活profile,否则按区分大小写方式匹配属性值字段 -->
<property>
<!--激活profile的属性的名称 -->
<name>mavenVersion</name>
<!--激活profile的属性的值 -->
<value>2.0.3</value>
</property>
<!--提供一个文件名,通过检测该文件的存在或不存在来激活profile。missing检查文件是否存在,如果不存在则激活profile。另一方面,exists则会检查文件是否存在,如果存在则激活profile。 -->
<file>
<!--如果指定的文件存在,则激活profile。 -->
<exists>${basedir}/file2.properties</exists>
<!--如果指定的文件不存在,则激活profile。 -->
<missing>${basedir}/file1.properties</missing>
</file>
</activation>

注:在maven工程的pom.xml所在目录下执行mvn help:active-profiles命令可以查看中央仓储的profile是否在工程中生效。

Properties

作用:对应profile的扩展属性列表。
maven属性和ant中的属性一样,可以用来存放一些值。这些值可以在pom.xml中的任何地方使用标记${X}来使用,这里X是指属性的名称。属性有五种不同的形式,并且都能在settings.xml文件中访问。

  1. env.X: 在一个变量前加上"env."的前缀,会返回一个shell环境变量。例如,"env.PATH"指代了$path环境变量(在Windows上是%PATH%)。
  2. project.x:指代了POM中对应的元素值。例如: <project><version>1.0</version></project>通过${project.version}获得version的值。
  3. settings.x: 指代了settings.xml中对应元素的值。例如:<settings><offline>false</offline></settings>通过 ${settings.offline}获得offline的值。
  4. Java System Properties: 所有可通过java.lang.System.getProperties()访问的属性都能在POM中使用该形式访问,例如 ${java.home}
  5. x: 在<properties/>元素中,或者外部文件中设置,以${someVar}的形式使用。
1
2
3
<properties>
<user.install>${user.home}/our-project</user.install>
</properties>

注:如果该profile被激活,则可以在pom.xml中使用${user.install}

Repositories

作用:远程仓库列表,它是maven用来填充构建系统本地仓库所使用的一组远程仓库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<repositories>
<!--包含需要连接到远程仓库的信息 -->
<repository>
<!--远程仓库唯一标识 -->
<id>codehausSnapshots</id>
<!--远程仓库名称 -->
<name>Codehaus Snapshots</name>
<!--如何处理远程仓库里发布版本的下载 -->
<releases>
<!--true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 -->
<enabled>false</enabled>
<!--该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。这里的选项是:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 -->
<updatePolicy>always</updatePolicy>
<!--当Maven验证构件校验文件失败时该怎么做-ignore(忽略),fail(失败),或者warn(警告)。 -->
<checksumPolicy>warn</checksumPolicy>
</releases>
<!--如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置,POM就可以在每个单独的仓库中,为每种类型的构件采取不同的策略。例如,可能有人会决定只为开发目的开启对快照版本下载的支持。参见repositories/repository/releases元素 -->
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
<!--远程仓库URL,按protocol://hostname/path形式 -->
<url>http://snapshots.maven.codehaus.org/maven2</url>
<!--用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。Maven 2为其仓库提供了一个默认的布局;然而,Maven 1.x有一种不同的布局。我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。 -->
<layout>default</layout>
</repository>
</repositories>

PluginRepositories

作用:发现插件的远程仓库列表。
和repository类似,只是repository是管理jar包依赖的仓库,pluginRepositories则是管理插件的仓库。
maven插件是一种特殊类型的构件。由于这个原因,插件仓库独立于其它仓库。pluginRepositories元素的结构和repositories元素的结构类似。每个pluginRepository元素指定一个Maven可以用来寻找新插件的远程地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<pluginRepositories>
<!-- 包含需要连接到远程插件仓库的信息.参见profiles/profile/repositories/repository元素的说明 -->
<pluginRepository>
<releases>
<enabled />
<updatePolicy />
<checksumPolicy />
</releases>
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
<id />
<name />
<url />
<layout />
</pluginRepository>
</pluginRepositories>

ActiveProfiles

作用:手动激活profiles的列表,按照profile被应用的顺序定义activeProfile。
该元素包含了一组activeProfile元素,每个activeProfile都含有一个profile id。任何在activeProfile中定义的profile id,不论环境设置如何,其对应的 profile都会被激活。如果没有匹配的profile,则什么都不会发生。
例如,env-test是一个activeProfile,则在pom.xml(或者profile.xml)中对应id的profile会被激活。如果运行过程中找不到这样一个profile,Maven则会像往常一样运行。

1
2
3
4
5
6
7
8
9
10
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0
https://maven.apache.org/xsd/settings-1.0.0.xsd">
...
<activeProfiles>
<!-- 要激活的profile id -->
<activeProfile>env-test</activeProfile>
</activeProfiles>
...
</settings>

打包与 FatJar

什么是 FatJar

FatJar 又叫 uber-jar。uber 不是打车的 uber,而是德语里面的 uber,意思是英语里面的 over-勉强可以翻译为超越。

FatJar 是一个 all-in-one 的 jar,它可以让部署和运行更加便利,它让最终部署和运行的环境不依赖于任何 maven 或者 lib 的 classpath。

FatJar 的三种具体类型

graph TB
    subgraph 普通JAR
        A1[META-INF]
        A2[com/example/*.class]
        A3[依赖外部classpath]
    end
    
    subgraph Unshaded FatJar
        B1[META-INF]
        B2[com/example/*.class]
        B3[org/apache/commons/*.class]
        B4[所有依赖解压平铺]
    end
    
    subgraph Shaded FatJar
        C1[META-INF]
        C2[com/example/*.class]
        C3[com/acme/shaded/apachecommons/*.class]
        C4[依赖类被重命名]
    end
    
    subgraph Spring Boot FatJar
        D1[META-INF]
        D2[BOOT-INF/classes/*.class]
        D3[BOOT-INF/lib/*.jar]
        D4[org/springframework/loader/*.class]
    end

非遮蔽的(Unshaded)—— maven-assembly-plugin

依赖于maven-assembly-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>assemble-all</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>

descriptorRef 有:

  • bin 只打包编译结果,并包含 README, LICENSE 和 NOTICE 文件,输出文件格式为 tar.gz, tar.bz2 和 zip。
  • jar-with-dependencies 打包编译结果,并带上所有的依赖,如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar。
  • src 打包源码文件。输出格式为 tar.gz, tar.bz2 和 zip。
  • project 打包整个项目,除了部署输出目录 target 以外的所有文件和目录都会被打包。输出格式为 tar.gz, tar.bz2 和 zip。

所有的 jar 都被 unpack,然后 repack。和 java 的缺省类加载器一起工作。

maven-assembly-plugin 也可以用于创建自定义分发包,将项目打包成 zip、tar.gz 等格式,包含配置文件、脚本等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.4.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>com.example.Main</mainClass>
</manifest>
</archive>
</configuration>
</execution>
</executions>
</plugin>

遮蔽的(Shaded)—— maven-shade-plugin

依赖于maven-shade-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<relocations>
<relocation>
<pattern>org.apache.commons</pattern>
<shadedPattern>com.acme.shaded.apachecommons</shadedPattern>
</relocation>
</relocations>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>

所有的 jar 都被 unpack,然后 repack,而且被刻意 rename(所以叫 shade),以避免 dependency version clashes。这种 rename 会产生字节码级的变动,使得类的 package 变化。

和 java 的缺省类加载器一起工作。

shaded jar 依然有可能导致版本冲突,所以需要依赖 class-relocation 解决类重定位的问题,依赖 Resource Transformers 解决资源重定位的问题。

shaded jar 是为了解决这样的问题:

A 依赖 b/c,b/c依赖于 d 的两个版本。如果不产生 shaded,则 b 和 c 必有一段分支路径运行时失败。如果让 b 使用 shaded 过的 d,产生一个 shaded-b,则这个 shaded-b 本身包含引用了改名的 d,c 依赖于正常的 d,两段执行路径都可要正常运行完。

JAR of JARs

只是把 jar 打包在一起,jar 里有 jar。

我们常见的 maven package 无插件操作打出来的 jar 就是这种 jar。

默认的 fatjar 里不一定包含所有的依赖,所以需要使用插件:

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>com.jolira</groupId>
<artifactId>onejar-maven-plugin</artifactId>
<version>1.4.4</version>
<executions>
<execution>
<goals>
<goal>one-jar</goal>
</goals>
</execution>
</executions>
</plugin>

Jar 的分类

  • Skinny – Contains ONLY the bits you literally type into your code editor, and NOTHING else.
  • Thin – Contains all of the above PLUS the app’s direct dependencies of your app (db drivers, utility libraries, etc).
  • Hollow – The inverse of Thin – Contains only the bits needed to run your app but does NOT contain the app itself. Basically a pre-packaged “app server” to which you can later deploy your app, in the same style as traditional Java EE app servers, but with important differences.
  • Fat/Uber – Contains the bit you literally write yourself PLUS the direct dependencies of your app PLUS the bits needed to run your app “on its own”.

Spring Boot 与 FatJar

实际上 Java 的原生类加载器处理普通的 Jar 里面的嵌套 class 是友好的,但处理嵌套的 jar 是不友好的。

Spring Boot 的 jar 就是 fatjar,这种 fatjar 携带所有依赖,而且有专有的类加载器来处理嵌套 jar 的依赖问题。这种 fatjar 是最简单的,运行起来最友好的。

分析依赖本来要逐层解压这种 jar-of-jars,但很多解析工具只解析一层的话,反而会被其他问题触发。例如,有时候为了解决依赖版本冲突而指定 jar 的版本,而直接在一个多模块的 parent pom 里面短路地指定了一个依赖版本,反而会触发解析工具的检测规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>1.3.1.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

<!-- 使用本插件打包非 Spring-Boot 专有程序 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout>
<requiresUnpack>
<dependency>
<groupId>org.jruby</groupId>
<artifactId>jruby-complete</artifactId>
</dependency>
</requiresUnpack>
</configuration>
</plugin>

这样会打印出两个 jar,一个 jar 是普通 jar,另一个 jar 是 jar.original。第二个 jar 是原始 jar,而第一个 jar 则是大而全的真正的 fat-jar。

测试与覆盖率

Apache Maven Surefire

Apache Maven Surefire 本身是一个测试框架。Maven Surefire Plugin 和 Maven Failsafe Plugin 都是这个项目的模块。

Surefire 插件

Surefire 是在 maven 的构建生命周期里面,test phase 执行单元测试的插件。
Surefire 的意思是"完全,一定成功的"。任何单元测试失败,都会导致构建失败。
Surefire 跑测试失败,会在现场留下名如hs_err*的文件。

用法

这个插件只有一个 goal,就是 test。

因此,使用它都不需要配置什么 configuration 和 phase。

1
2
3
4
5
6
7
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
</plugins>

它默认就会在 test phase 被执行:

1
mvn test

不管底层的 test provider 是 JUnit 还是 TestNG,甚至只是一个 pojo,它都可以运行测试。

对于pojo,插件可以自动运行类里面的 public testxxx 方法,也可以使用 JDK 1.4 时代以后使用的断言。

对于 JUnit 之类的 test provider,可以使用以下配置进行并行测试:

1
2
3
4
5
6
7
8
9
10
11
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
<configuration>
<parallel>methods</parallel>
<threadCount>10</threadCount>
</configuration>
</plugin>
</plugins>

类路径太长问题

整体而言,就是有些 OS 的命令行不允许太长的类路径。

解决这个问题的思路是,不在命令行内传递类路径,而使用进程内部的参数传递机制。由此诞生的解决方案有:

  1. 单独的类加载器。这样可以通过执行一个伪的 booter 程序来绕开类路径的直接限制,在程序内部再 load 我们的目标程序。这个方法也有缺点。首先,java.class.path 这个系统变量不会包括启动 jar。如果应用程序需要关心这个点,可能会导致问题。其次,程序内部可能会调用Classloader.getSystemClassLoader()而不是使用缺省的类加载器(也就是我们这个 isolated 类加载器)来加载特定的类。

  2. 使用一个 Manifest-Only 的 Jar。这个 Jar 几乎为空,只有一个MANIFEST.MF 文件。JVM 会把这个清单文件里的类路径 honor 起来当做 directive。比如我们可以用一个单独的 Class-Path 的 attribute 来填写我们的很长的类路径。

Failsafe 插件

Failsafe 是在 maven 的构建生命周期里面,test phase 执行集成测试的插件。
Failsafe 看起来是 Surefire 的同义词,但事实上它使用更安全的方式运作。集成测试的失败,和构建过程解耦了,构建不会因此失败。

与 surefire-plugin 的区别在于:

  • surefire-plugin:运行单元测试(*Test.java
  • failsafe-plugin:运行集成测试(*IT.java
1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.0.0-M7</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>

JaCoCo

JaCoCo 本质上是是一个 java agent,基于 Java 的 Instrumentation API。可以对类文件进行 on-the-fly 的 instrumentation。它是在 class loading 的阶段通过 in memory 字节码增强的形式,进行类型补强。

JaCoCo 本身支持三种数据采集模式:

  • 写入文件系统
  • 作为一个 server 让其他客户端采集
  • 作为一个 client 去连接某个 TCP 端点获取数据

JMX 的接口本身是没有鉴权和授权机制的,所以使用的时候要分清什么是 trusted server。

JaCoCo java agent

JaCoCo 以一个 jar 的形式发布。下载地址,注意它的发布包有 osgi bundle的版本。当做 javaagent 使用,我们应该使用jacocoagent.jar,如果使用它的命令行接口,我们应该使用 jacococli.jar。它遵循通常的 Java agent 的启动命令语法:

1
-javaagent:[yourpath/]jacocoagent.jar=[option1]=[value1],[option2]=[value2]

javaagent 可用的 options 见这个表格

JaCoCo Maven plugin

要使用 jacoco 的插件,第一步是获取 maven 的的引用:

1
2
3
4
5
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.3-SNAPSHOT</version>
</plugin>

这个插件自带的 goal 分别是:

  • help
  • prepare-agent(最重要的 goal,大部分情况下只使用这个 goal)
  • prepare-agent-integration
  • merge
  • report-aggregate
  • check
  • dump
  • instrument
  • restore-instrumented-classes

使用这个插件最起码要配的属性:

1
2
3
4
5
6
7
8
9
10
11
<!-- pom 全局的 properties -->
<properties>
<!-- test相关,解决sonar上独立集成测试工程得不到测试覆盖率的问题 -->
<sonar.core.codeCoveragePlugin>jacoco</sonar.core.codeCoveragePlugin>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
<sonar.jacoco.itReportPath>target/jacoco-it.exec</sonar.jacoco.itReportPath>
<sonar.jacoco.reportPath>target/jacoco-ut.exec</sonar.jacoco.reportPath>
<!-- jacoco 是一定要全局配置一个输出 report 的路径的,这里的 jacoco-ut.exec 不是一个可执行文件,而是一个 report 的destFile -->
<jacoco.path>${project.build.directory}/jacoco-ut.exec</jacoco.path>
<jacoco.skip>true</jacoco.skip>
</properties>

然后在使用插件的时候,我们引用了这个 path

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.6.0.201210061924</version>
<configuration>
<!-- 这个到底有什么用,存疑。仅仅是越过 jacoco 当前的执行吗? -->
<skip>${jacoco.skip}</skip>
<!-- 复用刚才的数据落点资料 -->
<destFile>${jacoco.path}</destFile>
<dataFile>${jacoco.path}</dataFile>
<sessionId>jacoco_coverage</sessionId>
</configuration>
<executions>
<execution>
<id>pre-test</id>
<!--在 compile 完成后,处理类的 phase-->
<phase>process-classes</phase>
<goals>
<!-- 准备 agent -->
<goal>prepare-agent</goal>
</goals>
<configuration>
<!-- 配置好的 agent,要准备放到这个 maven property 里面,这样就可以被其他插件(特别是测试用的 surefire 插件)复用 -->
<propertyName>coverageAgent</propertyName>
</configuration>
</execution>
</executions>
</plugin>

上面这个 goal 的主要用处就是准备一个 maven property,用来给其他插件作为 VM argument。在这个例子里,argLine 是一个专门为 surefire 准备的 maven property。

Surefire 与 JaCoCo 集成

surefire 插件本身就可以把 JaCoCo 生成的 maven property 内插进自己的命令行参数里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<argLine>-Xmx1024m -XX:PermSize=256m -XX:MaxPermSize=256m
${coverageAgent}</argLine>
<testFailureIgnore>true</testFailureIgnore>
<includes>
<include>**/*Test*.java</include>
</includes>
</configuration>
</plugin>

更详细的配置(包含如何写 report)见 Creating Code Coverage Reports for Unit and Integration Tests

参考资料