Maven核心概念

最近决定在自己的一个项目中使用了Akka框架,而Akka的Scala语法写起来要更自然一些,于是尝试使用Scala语言替代Java。Scala虽然是基于JVM的语言,其也有自己的一套技术栈,其中就包括构建工具SBT(Simple Build Tool)。在学习SBT过程中,不由思考,在Java平台,当今使用最广泛的构建工具仍然是Maven。Maven做为Ant的替代者,在2004年发布了第一版,至今十多年时间里,有很多负面的声音,也出现了很多的替代工具如Ivy,Gradle等,但是Maven仍然能被广泛的使用,只能说明Maven虽有不足,但在大多情况下已经足够好了。温故而知新,花了一些时间读了一下Maven的官方文档,整理所得遂成此文。

惯例优先原则

大多构建工具都是任务(Task)驱动的,任务之间相互依赖,最终完成完整的构建过程,比如Make、Ant、Gulp、Rake等。这类任务驱动的构建工具,概念简单,用途广泛,可以通过定义不同的任务与依赖关系,完成各种各样的构建工作,比如Gulp在构建Web前端工程的时候并没有任何的默认流程,每个项目都可以根据自己的需要自行拼装各种插件。

传统的任务驱动的构建工具有一个缺点,对于每一个新项目,都要从零开始写构建文件,相似的项目,不同的人,可以写出截然不同的构建行为。这样不但增加了构建文件的复杂性与维护成本,也增加了新成员对构建系统的学习成本。在Maven出现前,Java项目的构建主要使用Ant,如果您经历过使用Ant的年代,就一定会记得那时Java项目的目录结构千奇百怪,依赖、打包与发布的方式也各不相同,要了解一个项目,可能先要从阅读它复杂的构建文件开始。

Maven通过实践惯例优先原则(convention over configuration),从根本上解决了在此之前出现的构建碎片化的问题。Maven总结了Java项目的一般构建经验,内置了一套标准的构建惯例,包括目录的结构、依赖管理、构建流程等。如果一个项目的构建方式与Maven的惯例相似,就可以通过极少的配置完成整个项目的构建。在实践中,Java项目的构建方式区别不大,所以Maven可以很好的满足需要。从另一个角度看,Maven的出现规范了Java项目的构建,之后出现的几个Java构建工具也从Maven那里继承了这些惯例,现如今,不论是GitHub上的开源项目,还是公司的私有项目,其目录结构与构建方式都差不多,这样就节省了开发人员的学习成本。

惯例优先的配置方式使得标准化的构建非常简便,因为它将大量的信息隐藏在了工具内部,当需要对Maven的惯例进行一些定制的时候,甚或使用完全不同的构建流程的时候,那不但需要了解Maven的配置方式,还要下功夫去搞懂Maven的设计理念,因此Maven入门简单但是进阶的学习曲线要更陡峭一些。

* Maven之后出现的很多任务驱动的构建工具也内置了默认的构建定义或使用脚手架类的方式解决了碎片化的问题,但这已经超出本文的内容范畴。

核心概念

在Maven中最重要的三个概念是:Build Lifecycle(构建生命周期)、Phase(阶段)与Goal(目标)。Lifecycle指由一系列Phase组成的一个构建流程,它可以具象的理解为Phase的有序集合。Phase指构建流程中的每一个具体的构建阶段,Phase本身不做任何工作,可以理解为构建流程中的挂载点,可以将具体的工作(Goal)绑定到某一个Phase,这样在构建到这个Phase的时候,就会顺序执行绑定的工作。Goal即指实际的工作,与其他构建系统中的任务(Task)相同,可以是编译源代码、测试、打包等工作。

Maven是自顶向下的,它要求首先设计好构建的流程,然后才在每一阶段绑定具体的工作。而传统的构建工具往往是自低向上的,可以先写具体的任务,然后再考虑它们之间的依赖关系,最后形成一个完成的流程。

Maven内置了三个Lifecycle,分别为CleanDefaultSite。对应项目构建前的清理流程,项目的构建流程与项目报告、站点的生成流程。每一个Lifecycle中都预定义了一组Phase。下边列出了内置的三个Lifecycle与其下的Phase

  • Clean: pre-clean、clean、post-clean
  • Default: validate、initialize、generate-sources、process-sources、generate-resources、process-resources、compile、process-classes、generate-test-sources、process-test-sources、generate-test-resources、process-test-resources、test-compile、process-test-classes、test、prepare-package、package、pre-integration-test、integration-test、post-integration-test、verify、install、deploy
  • Site: pre-site、site、post-site、site-deploy

Lifecycle中的每一个Phase都依赖前一个Phase,当输入mvn package的时候,Maven会从validateinitialize一路运行到package阶段。不同Lifecycle内的Phase相互独立,没有依赖关系,所以如果要先清理再打包,需要运行mvn clean package

Phase只是Goal的挂载点,不做任何实际的工作,如果一个Phase中没有绑定任何的Goal,在运行的时候这个Phase就什么也不做。一个Phase的名字叫compile不代表它实际上一定会做编译的工作,如果上边绑定的是测试的Goal,它就会做测试的工作。

Maven内置了一些Goal,并根据用户对<packaging>的设置做默认的绑定。比如当<packaging>jar的时候,Maven会做如下的绑定:

Phase Goal
process-resources resources:resources
compile compiler:compile
process-test-resources resources:testResources
test-compile compiler:testCompile
test surefire:test
package jar:jar
install install:install
deploy deploy:deploy

所有Maven内置的LifecyclePhaseGoal的定义都可以在maven-core-x.x.x.jar中的META-INF路径下找到。也可以通过插件的方式创建新的Lifecycle

POM文件

使用Maven构建的项目都需要在根目录下包含一个XML格式的POM(Project Object Model)文件pom.xml。一个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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<!-- 基本设置 -->
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<packaging>...</packaging>
<dependencies>...</dependencies>
<parent>...</parent>
<dependencyManagement>...</dependencyManagement>
<modules>...</modules>
<properties>...</properties>

<!-- 构建设置 -->
<build>...</build>
<reporting>...</reporting>

<!-- 项目相关信息 -->
<name>...</name>
<description>...</description>
<url>...</url>
<inceptionYear>...</inceptionYear>
<licenses>...</licenses>
<organization>...</organization>
<developers>...</developers>
<contributors>...</contributors>

<!-- 环境设置 -->
<issueManagement>...</issueManagement>
<ciManagement>...</ciManagement>
<mailingLists>...</mailingLists>
<scm>...</scm>
<prerequisites>...</prerequisites>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<distributionManagement>...</distributionManagement>
<profiles>...</profiles>
</project>

其中的groupIdartifactIdversion合在一起组成了项目唯一的坐标(Coordinates),其一般的表现形式为groupId:artifactId:version,是Maven项目继承与相互依赖的唯一标识。

继承

POM文件可以被继承,子POM文件会继承所有父POM文件中的配置。处于继承关系最顶端的是Maven内置的Super POM,其位于maven-model-builder-x.x.x.jar中的org/apache/maven/model/pom-4.0.0.xml中。通过继承这些配置,一个最简的POM文件内容可以只包含4个项目:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>

</project>

经过层层继承与重写之后,有时很难确定一个配置的值是什么,这时可以通过命令mvn help:effective-pom打印出合并后的最终配置。

POM继承常被用于多个项目有共通配置的情况。将共通的配置提取到单独的POM文件中,由所有项目继承,以达到方便修改与维护的目的。SpringBoot就是一个很好的例子,因为它有大量的外部依赖,一些特有的项目配置与插件,使用这个框架的项目很可能会在不知情的情况下使用了不兼容的依赖版本,或者没有按照框架的要求进行配置,造成运行时错误,所以它建议项目都继承他的POM定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>myproject</artifactId>
<version>0.0.1-SNAPSHOT</version>

<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
</project>

SpringBoot的POM继承的另一个优势为,当框架升级的时候,外部的依赖也会跟着升级,而继承POM的项目只需要改一下继承的框架版本即可,大量的依赖升级与配置变更对用户完全是透明的。

聚合

另一个常用的POM特性是聚合(Project Aggregation)。在父POM文件中指定子项目的列表,这样在父项目中运行Maven命令,就会同时在所有子项目中运行同样的命令。比如修改了某一个子项目的代码,在父项目中运行mvn test,就可以测试所有的子项目,这样不但被修改的项目会被测试,对它有依赖的项目也会被测试。下边是一个聚合的配置例子:

1
2
3
4
5
6
7
8
9
10
11
12
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>name.liangshuang</groupId>
<artifactId>maven</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>

<modules>
<module>module-1</module>
<module>module-2</module>
</modules>
</project>

在多子项目的情景中,继承与聚合往往是搭配使用的,这样既可以将共通的配置集中管理,又可以很方便的在所有项目上运行相同的构建命令。

Profile

从4.0版本开始,Maven加入了对Profile的支持。可以在一个POM文件(或配置文件)中定义多个Profile,并根据特定的环境激活不同的Profile,以达到对不同情景使用不同构建方式的目的。比如,对于集成测试环境与生产环境构建使用不同的配置文件,或者对于不同的JDK版本使用不同的依赖。

Profile的定义可以包含下边的内容:

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
<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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
...
<profiles>
<profile>
<id>someid</id> <!-- 唯一的ID用于在命令行下激活 -->
<activation>
<!-- 指定在什么条件下激活此Profile,下边的条件只要有一个符合,此Profile即被激活 -->
<activeByDefault>false</activeByDefault> <!-- 是否是默认的Profile -->
<jdk>1.5</jdk>
<os>
<name>Windows XP</name>
<family>Windows</family>
<arch>x86</arch>
<version>5.1.2600</version>
</os>
<property>
<name>sparrow-type</name>
<value>African</value>
</property>
<file>
<exists>${basedir}/file2.properties</exists>
<missing>${basedir}/file1.properties</missing>
</file>
</activation>
<build>...</build>
<modules>...</modules>
<repositories>...</repositories>
<pluginRepositories>...</pluginRepositories>
<dependencies>...</dependencies>
<reporting>...</reporting>
<dependencyManagement>...</dependencyManagement>
<distributionManagement>...</distributionManagement>
</profile>
</profiles>
</project>

在命令行下可以使用参数mvn -P profileId ....来指定激活的Profile,也可以使用命令mvn help:active-profiles来查看当前激活的Profile

Archetype

Archetype是一个项目模版工具,可以用来生成特定的项目结构与基本的POM文件。Maven官方提供了11个模板,其中比较常用的是maven-archetype-quickstartmaven-archetype-webapp,前者用来生成一个最基本的Maven项目,后者用来生成Web项目。通过运行如下命令使用Archetype:

1
2
3
4
mvn archetype:generate                                  \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DgroupId=name.liangshuang \
-DartifactId=maven

插件

Maven是由上边谈到的核心框架与各种各样的插件(plugin)搭配来完成工作的。插件提供了Maven中的目标,是完成最终工作的部件。插件有两种用法:

一种方式是将插件的goal绑定到某个phase,这样在运行到这个phase的时候这个goal就会被调用,很多插件的goal都有默认的绑定,这些插件在POM文件中可以省略绑定的配置。

绑定是推荐的使用方法,因为很多的任务并不是独立运行的,往往需要依赖其他任务的运行结果,比如打包任务需要在源代码编译之后进行,否则只会得到一个空的jar包,而把打包的goal绑定到compile之后的phase,运行其绑定的phase就可以在打包之前完成需要的前置工作。

另一种是直接运行goal。可以通过完整的坐标groupId:artifactId:version:goal来运行,如mvn sample.plugin:hello-maven-plugin:1.0-SNAPSHOT:sayhi。其中,版本号可以省略,前边的groupId:artifactId可以用短前缀表示。短前缀指插件名的前缀部分,即<yourplugin>-maven-plugin中的yourplugin。另一种前缀的命名方式是maven-<yourplugin>-plugin,一般只有Mavne官方的插件才会这样命名。省略之后的形式就是常在文档中看到运行方式:mvn yourplugin:somegoal。脱离lifecycle,通过goal直接运行任务,需要注意对其他任务结果的依赖问题。

外部插件在使用前需要先被引入项目中,最常用的方式是在POM文件中进行配置,下边以Ant插件为例:

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
<build>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId> <!-- 根据名称可以判断这是Mavne官方的插件,短前缀为antrun -->
<version>1.8</version>
<executions>
<execution>
<id>msgone</id> <!-- 设置ID后可以通过antrun:run@msgone运行 -->
<configuration>
<target>
<echo message="One"/>
</target>
</configuration>
<goals>
<goal>run</goal> <!-- 这里要对应插件提供的goal -->
</goals>
</execution>
<execution> <!-- 不设置ID就是default(默认),ID不能重复,default也不例外 -->
<phase>compile</phase> <!-- 绑定到compile阶段 -->
<configuration>
<target>
<echo message="Two"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
<execution>
<id>msgthree</id> <!-- ID不能重复,所以需要设置一个与以上两个ID不同的值 -->
<phase>test</phase> <!-- 绑定到test阶段 -->
<configuration>
<target>
<echo message="Three"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

Maven不允许一个项目引用多个相同的插件,但一个插件可以定义多个<execution>,每一个<execution>可以使用不同的配置,需要有唯一的<id>。上边的第一个<execution>没有绑定到任何的phase,可以通过命令mvn antrun:run@msgone运行,结果为One。后两个<execution>都与phase做了绑定,如果运行mvn compile只会打印Two,运行mvn test会先后打印TwoThree,因为testcompile阶段同在defaultlifecycle中,并且排在后边。

依赖管理

Maven的仓库与自动依赖管理是其核心功能之一,为复杂项目的构建工作带来了巨大的便利。其强大之处在于,它可以递归分析依赖关系,尝试解决版本冲突问题,并自动扫描本地仓库或连接远程仓库下载所有依赖的软件包。

版本冲突

Maven在递归的分析所有的依赖关系之后,会生成一个依赖树,树的第一级是直接的依赖,第二级是直接依赖的依赖,以此类推,可以运行命令mvn dependency:tree查看依赖树。当在这棵依赖树中存在对同一个软件包的不同版本的依赖时,就出现了版本冲突的问题。在出现冲突时,Maven优先使用级别更靠前(更直接)的依赖版本。当冲突的软件包同级时,会根据出现的先后顺序进行选择。自动冲突解决的结果并不总是正确的,比如低版本的软件包如果更靠前,就会被选择,而没被选定的高版本可能不但兼容低版本而且提供了新的功能或改正一些错误,这时明显应该选择后者。通过在POM文件中显式指定软件包的版本,可以避免版本冲突带来的不确定性。

作用域

Maven通过给依赖的定义添加作用域(Dependency Scope)来限制依赖的传递性。依赖作用域共有6个:

  • compile: 这是默认的作用域,在所有的classpath中都可以见,并且会被向上传递。
  • provided: 与compile作用域相似,唯一的区别是这个依赖在运行时应由容器提供,比如Servlet API常由运行的容器如Tomcat提供。provided的依赖只在编译与测试的classpath中可见,不会向上传递。
  • runtime: 表示在编译的时候不需要该依赖,只在运行的时候需要。它只在运行时与测试的classpath中可见。比如应用虽然使用的是MySQL数据库,但却没有对MySQL驱动的直接调用,而是通过JDBC API来操作MySQL,这时MySQL驱动就应该被定义为runtime依赖。
  • test: 测试用的依赖。只在测试的编译与运行时可见,不会向上传递。如JUnit等依赖都使用这个作用域。
  • system: 与provided相似,唯一的区别是需要通过<systemPath>指定依赖的路径。这个作用域已经不建议使用了(deprecated)。
  • import: 只能用在<dependencyManagement>之中,用来引入另一个POM项目中的依赖定义,不会影响依赖的传递性。这种用法比较少见,可以参考下边SpringBoot文档中的用法:
1
2
3
4
5
6
7
8
9
10
11
12
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.5.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

可选依赖与依赖排除

除了通过作用域来改变依赖的传递,还可以使用可选依赖(Optional Dependencies)与依赖排除(Dependency Exclusions)来阻止依赖的传递。

可选依赖需要在依赖定义中将<optional>设为true,常常用于项目有多个功能,但又没有必要划分成多个子项目的情景。当一个项目依赖于一个有可选依赖的软件包,所有的可选依赖都不会自动成为该项目的依赖,而是需要根据所需的功能,自行在POM文件中显式的定义依赖。下边是一个可选依赖的例子:

1
2
3
4
5
6
7
8
9
10
11
12
<project>
...
<dependencies>
<dependency>
<groupId>sample.ProjectA</groupId>
<artifactId>Project-A</artifactId>
<version>1.0</version>
<scope>compile</scope>
<optional>true</optional> <!-- 配置为可选依赖 -->
</dependency>
</dependencies>
</project>

可选依赖是定义在被依赖方的,而依赖排除则是定义在依赖方的,作用与可选依赖相似,有时也被用来解决依赖的冲突问题。下边是依赖排除的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<project>
...
<dependencies>
<dependency>
<groupId>sample.ProjectA</groupId>
<artifactId>Project-A</artifactId>
<version>1.0</version>
<scope>compile</scope>
<exclusions>
<exclusion> <!-- 排除对sample.ProjectB:Project-B的依赖 -->
<groupId>sample.ProjectB</groupId>
<artifactId>Project-B</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

全局配置

Maven的配置文件为settings.xml,可以用来保存对所有项目都适用的一些配置。Maven会在两个位置搜索配置文件:

  • 全局配置,位于Maven的安装路径下:${maven.home}/conf/settings.xml
  • 用户配置,位于用户目录下:${user.home}/.m2/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>

下边介绍两个常用的配置:

Servers

Servers用于设置项目发布的服务器,比如公司内部的Nexus服务器或者OSS的服务器,下边是OSS服务器的配置例子:

1
2
3
4
5
6
7
8
9
10
11
<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">
...
<servers>
<server>
<id>ossrh</id>
<username>你的用户名</username>
<password><![CDATA[你的密码]]></password>
</server>
</servers>
</settings>

Mirrors

使用国外的Maven仓库,速度慢,也不稳定,可以使用阿里云提供的镜像或者公司内部的Nexus服务器,下边以阿里云的配置为例:

1
2
3
4
5
6
7
8
9
10
11
12
<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">
....
<mirrors>
<mirror>
<id>aliyun</id>
<name>Aliyun Mirror</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
<mirrorOf>central</mirrorOf> <!-- 也可以使用*代替central,表示对所有源都使用该镜像 -->
</mirror>
</mirrors>
</settings>

参考