Maven

Maven 作为一个构建工具,不仅能自动化构建项目,还能够抽象构建过程,提供构建任务实现;它跨平台,对外提供了一致的操作接口,这一切足以使它成为优秀的、流行的构建工具。Maven 不仅是构建工具,还是一个依赖管理工具和项目管理工具。它提供了中央仓库,且能按照配置自动下载构件。

Maven 坐标

项目中依赖的第三方库及插件可统称为构件(artifact),每个 artifact 都可用 Maven 坐标唯一标识。坐标元素包括:

  • 必须元素
    • groupId:定义了当前 Maven 项目隶属的组织或公司。groupId 一般分为多段,通常情况下,第一段为域,第二段为公司名称。
    • artifactId:定义了当前 Maven 项目的名称,项目的唯一的标识符,对应项目根目录的名称。
    • version:定义了 Maven 项目当前所处版本。
  • 可选元素
    • packaging(可选):定义了 Maven 项目的打包方式(如jar,war,……),默认为 jar
    • classifier(可选):常用于区分从同一 POM 构建的具有不同内容的构件,可以是任意的字符串,附加在版本号之后。

引入依赖,一般来说只需要指定其中三个坐标元素即可。

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>

Maven 依赖

依赖配置

Maven 依赖配置均在项目的 pom.xml 下。

配置信息示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<project>
<dependencies>
<dependency>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>
<groupId>...</groupId>
<artifactId>...</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

配置说明

  • dependencies:一个 pom.xml 文件中只能存在一个这样的标签,是用来管理依赖的总标签。
  • dependency:包含在 dependencies 标签中,可有多个,每个表示项目的一个依赖。
  • groupIdartifactIdversion:依赖的基本坐标。
  • 可选配置
    • type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下使用默认值 jar 即可。
    • scope:依赖的范围,默认值为 compile。
    • optional:标记依赖是否可选。
    • exclusions:用于排除传递性依赖,避免 jar 包冲突。

依赖范围

classpath 用于指定 .class 文件存放的位置,类加载器会从该路径中加载所需的 .class 文件到内存中。

Maven 在编译、执行测试、实际运行有着三套不同的 classpath:

  • 编译 classpath:编译主代码有效
  • 测试 classpath:编译、运行测试代码有效
  • 运行 classpath:项目运行时有效

Maven 的依赖范围如下:

  • compile(默认):编译依赖范围。使用该依赖范围对于编译、测试、运行三种都有效,即在编译、测试、运行的时候都要使用该依赖。
  • test:测试依赖范围。表明该依赖只能用于测试。
  • provided:对编译、测试有效,对运行无效。
  • runtime:运行时依赖范围。对测试和运行有效,但是在编译主代码时无效。
  • system:系统依赖范围。使用有该范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径,而不依赖 Maven 仓库解析,所以可能会造成构建的不可移植。

传递依赖

依赖冲突

  1. 同一个 groupId 和同一个 artifactId 下,只能使用一个 version。若有不同,则只会引入后一个声明的依赖。
1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>in.hocg.boot</groupId>
<artifactId>mybatis-plus-spring-boot-starter</artifactId>
<version>1.0.48</version>
</dependency>
<!-- 只会使用 1.0.49 这个版本的依赖 -->
<dependency>
<groupId>in.hocg.boot</groupId>
<artifactId>mybatis-plus-spring-boot-starter</artifactId>
<version>1.0.49</version>
</dependency>
  1. 项目的两个依赖同时引入了某个依赖

    即对于项目 A,存在下面的依赖关系:

graph LR
  A --> B --> C --> X_2.0
  A --> D --> X_1.0

依赖路径上有两个版本的 X,为了避免依赖重复,Maven 只会选择其中的一个进行解析。

Maven 依赖调解

在遇到上述问题的时候,Maven 会遵循 路径最短优先 和 声明顺序优先 两大原则来解决,解决的过程被称作 Maven 依赖调解

  • 路径最短优先

    根据上述的依赖路径,X 2.0 会被解析使用。这是因为第二条路径长度(2)小于第一条(3)。

    但是该原则也并非通用的,若遇到路径长度相等的情况,就需要使用声明顺序优先原则了。

graph LR
  A --> B --> Y_2.0
  A --> D --> Y_1.0
  • 声明顺序优先

    在依赖路径长度相等的前提下,在 pom.xml 中依赖声明的顺序决定了谁会被解析使用,顺序最前的那个依赖优胜。在上述例子中,若 B 的依赖声明在 D 之前,那么 Y 1.0 就会被解析使用。

1
2
3
4
5
6
7
8
<!-- A pom.xml -->
<dependencies>
...
dependency B
...
dependency D
...
</dependencies>

排除依赖

有时侯需要我们手动排除依赖来解决依赖冲突问题。

同样的例子,根据路径最短优先原则,X 1.0 会被解析使用。但是如果依赖 C 用到了 X 2.0 中才有的类或方法时,运行项目就会报 NoClassDefFoundErrorNoSuchMethodError 错误。

graph LR
  A --> B --> C --> X_2.0
  A --> D --> X_1.0

要解决该问题,需要在引入依赖时通过 exclusion 标签手动将 X 1.0 排除。

1
2
3
4
5
6
7
8
9
<dependency>
......
<exclusions>
<exclusion>
<artifactId>x</artifactId>
<groupId>group.x</groupId>
</exclusion>
</exclusions>
</dependency>

一般都会优先保留版本较高的,因为大部分 jar 在升级的时候都会做到向下兼容。

但如果出现 jar 不向下兼容的情况,应考虑升级其上层依赖。如同样的例子,若 X 2.0 删除了一些 X 1.0 的类或方法,那么要考虑升级依赖 D 的版本到一个 X 2.0 兼容的版本。

Maven 仓库

Maven 通过仓库来统一管理构件(artifact),每个构件都有一组坐标唯一标识。通过该坐标我们可以在仓库中找到该构件。

Maven 仓库分为:

  • 本地仓库:

    运行 Maven 的计算机上的一个目录,缓存有远程下载的构件并包括尚未发布的临时构件。settings.xml 文件中可以看到 Maven 的本地仓库路径配置,默认本地仓库路径是在 ${user.home}/.m2/repository

  • 远程仓库:

    官方或其他组织维护的 Maven 仓库。

Maven 远程仓库分为:

  • 中央仓库:

    该仓库由 Maven 社区维护,是 Maven 的默认远程仓库。另外为了方便查询,还提供了一个查询地址,开发者可以通过这个地址更快的搜索需要构件的坐标。Maven Repository

  • 私服:

    私服是一种特殊的远程 Maven 仓库,它是架设在局域网内的仓库服务,私服一般被配置为互联网远程仓库的镜像,供局域网内的 Maven 用户使用。

  • 其他公共仓库

    有一些公共仓库是为了加速访问(比如阿里云 Maven 镜像仓库)或者部分构件不存在于中央仓库中。

Maven 依赖包寻找顺序

  1. 先去本地仓库找,若有,直接使用。
  2. 若本地仓库没有找到,会去远程仓库找,并下载到本地仓库。
  3. 若远程仓库没有找到,会报错。

Maven 生命周期

Maven 的生命周期是为了对所有的构建过程进行抽象和统一,包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。

Maven 定义了 3 个生命周期 META-INF/plexus/components.xml

  • default
  • clean
  • site

这些生命周期是相互独立的,每个生命周期包含多个阶段(phase)。并且,这些阶段是有序的,也就是说,后面的阶段依赖于前面的阶段。当执行某个阶段的时候,会先执行它前面的阶段。

执行 Maven 生命周期的命令格式如下:

1
mvn 阶段 [阶段2] ... [阶段n]

default 生命周期

default 生命周期是在没有任何关联插件的情况下定义的,是 Maven 的主要生命周期,用于构建应用程序,共包含 23 个阶段。

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
<phases>
<!-- 验证项目是否正确,并且所有必要的信息可用于完成构建过程 -->
<phase>validate</phase>
<!-- 建立初始化状态,例如设置属性 -->
<phase>initialize</phase>
<!-- 生成要包含在编译阶段的源代码 -->
<phase>generate-sources</phase>
<!-- 处理源代码 -->
<phase>process-sources</phase>
<!-- 生成要包含在包中的资源 -->
<phase>generate-resources</phase>
<!-- 将资源复制并处理到目标目录中,为打包阶段做好准备。 -->
<phase>process-resources</phase>
<!-- 编译项目的源代码 -->
<phase>compile</phase>
<!-- 对编译生成的文件进行后处理,例如对 Java 类进行字节码增强/优化 -->
<phase>process-classes</phase>
<!-- 生成要包含在编译阶段的任何测试源代码 -->
<phase>generate-test-sources</phase>
<!-- 处理测试源代码 -->
<phase>process-test-sources</phase>
<!-- 生成要包含在编译阶段的测试源代码 -->
<phase>generate-test-resources</phase>
<!-- 处理从测试代码文件编译生成的文件 -->
<phase>process-test-resources</phase>
<!-- 编译测试源代码 -->
<phase>test-compile</phase>
<!-- 处理从测试代码文件编译生成的文件 -->
<phase>process-test-classes</phase>
<!-- 使用合适的单元测试框架(Junit 就是其中之一)运行测试 -->
<phase>test</phase>
<!-- 在实际打包之前,执行任何的必要的操作为打包做准备 -->
<phase>prepare-package</phase>
<!-- 获取已编译的代码并将其打包成可分发的格式,例如 JAR、WAR 或 EAR 文件 -->
<phase>package</phase>
<!-- 在执行集成测试之前执行所需的操作。 例如,设置所需的环境 -->
<phase>pre-integration-test</phase>
<!-- 处理并在必要时部署软件包到集成测试可以运行的环境 -->
<phase>integration-test</phase>
<!-- 执行集成测试后执行所需的操作。 例如,清理环境 -->
<phase>post-integration-test</phase>
<!-- 运行任何检查以验证打的包是否有效并符合质量标准。 -->
<phase>verify</phase>
<!-- 将包安装到本地仓库中,可以作为本地其他项目的依赖 -->
<phase>install</phase>
<!-- 将最终的项目包复制到远程仓库中与其他开发者和项目共享 -->
<phase>deploy</phase>
</phases>

根据前面提到的阶段间依赖关系理论,当我们执行 mvn test 命令的时候,会执行从 validate 到 test 的所有阶段,这也就解释了为什么执行测试的时候,项目的代码能够自动编译。

clean 生命周期

clean 生命周期的目的是清理项目,共包含 3 个阶段:

  1. pre-clean
  2. clean
  3. post-clean
1
2
3
4
5
6
7
8
9
10
11
12
13
<phases>
<!-- 执行一些需要在clean之前完成的工作 -->
<phase>pre-clean</phase>
<!-- 移除所有上一次构建生成的文件 -->
<phase>clean</phase>
<!-- 执行一些需要在clean之后立刻完成的工作 -->
<phase>post-clean</phase>
</phases>
<default-phases>
<clean>
org.apache.maven.plugins:maven-clean-plugin:2.5:clean
</clean>
</default-phases>

根据前面提到的阶段间依赖关系理论,当我们执行 mvn clean 的时候,会执行 clean 生命周期中的 pre-clean 和 clean 阶段。

site 生命周期

site 生命周期的目的是建立和发布项目站点,共包含 4 个阶段:

  1. pre-site
  2. site
  3. post-site
  4. site-deploy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<phases>
<!-- 执行一些需要在生成站点文档之前完成的工作 -->
<phase>pre-site</phase>
<!-- 生成项目的站点文档作 -->
<phase>site</phase>
<!-- 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备 -->
<phase>post-site</phase>
<!-- 将生成的站点文档部署到特定的服务器上 -->
<phase>site-deploy</phase>
</phases>
<default-phases>
<site>
org.apache.maven.plugins:maven-site-plugin:3.3:site
</site>
<site-deploy>
org.apache.maven.plugins:maven-site-plugin:3.3:deploy
</site-deploy>
</default-phases>

Maven 能够基于 pom.xml 所包含的信息,自动生成一个友好的站点,方便团队交流和发布项目信息。

Maven 插件

Maven 本质上是一个插件执行框架,所有的执行过程都由一个个插件独立完成。上述所使用到的 install、clean 等命令,底层都是一个个 Maven 插件。本地的默认插件路径为 ${user.home}/.m2/repository/org/apache/maven/plugins。关于 Maven 的核心插件可以参考 Maven Plugins

Maven 插件可以理解为一组任务的集合,用户可以通过命令行直接运行指定插件的任务,也可以将插件任务挂载到构建生命周期,随着生命周期运行。

Maven 插件被分为下面两种类型:

  • Build plugins:在构建时执行。
  • Reporting plugins:在网站生成过程中执行。

Maven 多模块管理

多模块管理简单地来说就是将一个项目分为多个模块,每个模块只负责单一的功能实现。直观的表现就是一个 Maven 项目中不止有一个 pom.xml 文件,会在不同的目录中有多个 pom.xml 文件,进而实现多模块管理。

多模块管理除了可以更加便于项目开发和管理,还有如下好处:

  1. 降低代码之间的耦合性(从类级别的耦合提升到 jar 包级别的耦合);
  2. 减少重复,提升复用性;
  3. 每个模块都可以是自解释的(通过模块名或者模块文档);
  4. 模块还规范了代码边界的划分,开发者很容易通过模块确定自己所负责的内容。

多模块管理下,会有一个父模块,其他的都是子模块。父模块通常只有一个 pom.xml,没有其他内容。父模块的 pom.xml 一般只定义了各个依赖的版本号、包含哪些子模块以及插件有哪些。不过,要注意的是,如果依赖只在某个子项目中使用,则可以在子项目的 pom.xml 中直接引入,防止父 pom 的过于臃肿。