前言

在没有使用Maven之前,我们开发一个JavaWeb项目,如果使用到非JDK提供的类库,需要去网上下载对应的jar包,然后将jar包复制粘贴放到项目的lib目录下才能够使用。这样的做法是很麻烦的,每开发一个JavaWeb项目都需要去将jar包文件放置在lib目录下,且只能以文件的形式进行管理。

我们可以使用Maven的依赖管理功能来减少复制jar包这样重复的工作。

什么是Maven

Maven是一个项目管理工具,可以对项目(不仅限于Java语言)进行构建和依赖管理。简单来说:使用了Maven之后我们就不需要再去下载复制粘贴jar包了。

Maven安装

Windows 10下Maven的安装及配置

Maven的约定配置

Maven提供约定优于配置的原则,对于一个使用Maven构建和依赖管理的项目,应该遵循以下的目录结构:

目录目录存放内容
${basedir}Maven项目根路径,存放pom.xml和所有的子目录
${basedir}/src/main/java项目的java类文件
${basedir}/src/main/resources项目的资源文件,例如properties配置文件,前端静态资源文件等
${basedir}/src/test/java项目的单元测试类文件,一般为Junit的单元测试类
${basedir}/src/test/resources提供给测试类使用的资源文件
${basedir}/src/main/webapp/WEB-INFJavaWeb项目的web应用文件目录,存放web.xml.jsp和前端静态资源等文件。
${basedir}/target打包输出目录
${basedir}/target/classes编译输出目录,java类编译后的字节码文件目录
${basedir}/target/test-classes测试类编译输出目录
XXXTest.javaMaven只会自动运行类名以Test结尾的测试类
~/.m2/repositoryMaven的默认本地仓库路径

pom.xml常用标签

下面是一个典型Spring Boot项目的pom.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
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 声明项目描述符遵循哪一个POM模型版本。模型本身的版本很少改变,虽然如此,但它仍然是必不可少的,这是为了当Maven引入了新的特性或者其他模型变更的时候,确保稳定性。 -->
<modelVersion>4.0.0</modelVersion>
<!-- 父项目的坐标。如果项目中没有规定某个元素的值,那么父项目中的对应值即为项目的默认值(继承)。 坐标包括group ID,artifact ID和version。-->
<parent>
<!-- 父项目的唯一组织名 -->
<groupId>org.springframework.boot</groupId>
<!-- 父项目的唯一构建标识符 -->
<artifactId>spring-boot-starter-parent</artifactId>
<!-- 父项目的版本号 -->
<version>2.4.0</version>
<!-- 父项目的pom.xml文件的相对路径。相对路径允许你选择一个不同的路径。默认值是../pom.xml。Maven首先在构建当前项目的地方寻找父项目的pom,其次在文件系统的这个位置(relativePath位置),然后在本地仓库,最后在远程仓库寻找父项目的pom。 -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- 当前项目的唯一组织名 -->
<groupId>com.sunchaser.demo</groupId>
<!-- 当前项目的唯一构建标识符 -->
<artifactId>springboot</artifactId>
<!-- 当前项目的版本号,格式为:主版本.次版本.增量版本-限定版本号 -->
<version>0.0.1-SNAPSHOT</version>
<!-- 当前项目的名称 -->
<name>springboot</name>
<!-- 当前项目的描述简介 -->
<description>Demo project for Spring Boot</description>

<!-- 以值(value)代替名称(name),可在整个pom.xml中使用,格式:<name>value<name/> -->
<properties>
<java.version>1.8</java.version>
</properties>

<!-- 发现依赖和扩展的远程仓库列表。 -->
<repositories>
<!-- 包含需要连接到远程仓库的信息 -->
<repository>
<!-- 如何处理远程仓库里发布版本的下载 -->
<releases>
<!-- true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 -->
<enabled />
<!-- 该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。这里的选项是:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。 -->
<updatePolicy />
<!-- 当Maven验证构件校验文件失败时该怎么做:ignore(忽略),fail(失败),或者warn(警告)。 -->
<checksumPolicy />
</releases>
<!-- 如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置,POM就可以在每个单独的仓库中,为每种类型的构件采取不同的策略。例如,可能有人会决定只为开发目的开启对快照版本下载的支持。参见repositories/repository/releases元素 -->
<snapshots>
<enabled />
<updatePolicy />
<checksumPolicy />
</snapshots>
<!--远程仓库唯一标识符。可以用来匹配在settings.xml文件里配置的远程仓库 -->
<id>aliyun</id>
<!--远程仓库名称 -->
<name>aliyun maven</name>
<!--远程仓库URL,按protocol://hostname/path形式 -->
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
<!-- 用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。Maven2为其仓库提供了一个默认的布局;然而,Maven 1.x有一种不同的布局。我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。 -->
<layout>default</layout>
</repository>
</repositories>

<!-- 该元素描述了项目相关的所有依赖。这些依赖组成了项目构建过程中的一个个环节。它们自动从项目定义的仓库中下载。要获取更多信息,请看项目依赖机制。 -->
<dependencies>
<!-- 由于parent的存在,以下依赖继承了parent中申明的版本号 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- 可选依赖,如果你在项目B中把C依赖声明为可选,你就需要在依赖于B的项目(例如项目A)中显式的引用对C的依赖。可选依赖阻断依赖的传递性。 -->
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<!-- 依赖范围。在项目发布过程中,帮助决定哪些构件被包括进来。欲知详情请参考依赖机制。
- compile :默认范围,用于编译
- provided:类似于编译,但支持你期待jdk或者容器提供,类似于classpath
- runtime: 在执行时需要使用
- test: 用于test任务时使用
- system: 需要外在提供相应的元素。通过systemPath来取得
- systemPath: 仅用于范围为system。提供相应的路径
- optional: 当项目自身被依赖时,标注依赖是否传递。用于连续依赖时使用
-->
<scope>test</scope>
</dependency>
</dependencies>

<!-- 构建项目需要的信息 -->
<build>
<!-- 使用的插件列表 -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

以上是将Spring Boot作为parentpom.xml,这样的好处是parent可以聚合依赖的版本等一些信息,在后面添加依赖时只需要填写groupIdartifactId坐标,版本号默认继承parent中定义的版本号。但很多时候我们的项目是Maven多模块项目,或者由于一些其它原因导致我们无法使用Spring Boot作为parent,这个时候就需要用到dependencyManagement标签来进行依赖预定义了,Spring Boot提供了BOM用来进行预定义依赖的引入,使用方式如下:

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
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.sunchaser.demo</groupId>
<artifactId>springboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<!-- 预定义依赖引入 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Maven的生命周期

Maven有三个标准的生命周期:

  • clean:项目清理的处理。
  • default(build):项目部署的处理。
  • site:项目站点文档创建的处理。

Clean生命周期

包含以下阶段:

  • pre-clean:执行一些需要在clean之前完成的工作。
  • clean:移除所有上一次构建生成的文件。
  • post-clean:执行一些需要在clean之后立即完成的工作。

我们所用的mvn clean命令就是上面的clean阶段。在一个生命周期中,运行某个阶段的时候,在它之前的所有阶段都会被运行。也就是说,如果执行maven clean命令将运行pre-cleanclean这两个生命周期阶段。

Default(build)生命周期

包含以下23个阶段:

  • validate校验:校验项目是否正确并且所有必要的信息可以完成项目的构建过程。
  • initialize初始化:初始化构建阶段,比如设置属性值。
  • generate-sources生成源代码:生成包含在编译阶段中的任何源代码。
  • process-sources处理源代码:处理源代码,比如过滤任意值。
  • generate-resources生成资源文件:生成将会包含在项目包中的资源文件。
  • process-resources处理资源文件:复制和处理资源到目标目录,为打包阶段做好准备。
  • compile编译:编译项目的源代码。
  • process-classes处理类文件:处理编译生成的文件,比如说对Java class文件做字节码改善优化。
  • generate-test-sources生成测试源代码:生成包含在编译阶段中的任何测试源代码。
  • process-test-sources处理测试源代码:处理测试源代码,比如说,过滤任意值。
  • generate-test-resources生成测试资源文件:为测试创建资源文件。
  • process-test-resources处理测试资源文件:复制和处理测试资源到目标目录。
  • test-compile编译测试源码:编译测试源代码到测试目标目录。
  • process-test-class处理测试类文件:处理测试源码编译生成的文件。
  • test测试:使用合适的单元测试框架运行测试(Junit是其中之一)。
  • prepare-package准备打包:在实际打包之前,执行任何的必要的操作为打包做准备。
  • package打包:将编译后的代码打包成可分发格式的文件,比如JARWAR或者EAR文件。
  • pre-integration-test集成测试前:在执行集成测试前进行必要的动作。比如说,搭建需要的环境。
  • integration-test集成测试:处理和部署项目到可以运行集成测试环境中。
  • post-integration-test集成测试后:在执行集成测试完成后进行必要的动作。比如说,清理集成测试环境。
  • verify验证:运行任意的检查来验证项目包有效且达到质量标准。
  • install安装:安装项目包到本地仓库,这样项目包可以用作其他本地项目的依赖。
  • deploy部署:将最终的项目包发布到远程仓库中与其他开发者和项目共享。

我们常用的mvn install命令,在执行install之前,按顺序执行了之前的21个阶段。如果用于多模块项目,每一个子项目都会执行mvn install命令。

还可以一次指定两个阶段,例如mvn clean deploy,这可以用来纯净的构建和部署项目到远程仓库中。Maven会先执行clean命令,再执行deploy命令,对多模块项目也适用。

Site生命周期

Maven Site插件一般用来创建新的报告文档和部署站点等。这个在我们的开发工作中一般不会用到,了解即可。

包含以下4个阶段:

  • pre-site:执行一些需要在生成站点文档之前完成的工作。
  • site:生成项目的站点文档。
  • post-site:执行一些需要在生成站点文档之后完成的工作,并且为部署做准备。
  • site-deploy:将生成的站点文档部署到特定的服务器上。

Maven仓库

Maven仓库简单的理解就是存储JAR包的地方。有以下三种类型:

  • 本地仓库(local
  • 中央仓库(central
  • 远程仓库(remote

本地仓库(local

运行Maven项目时,任何依赖的构建或第三方JAR包都是直接从本地仓库获取的。如果本地仓库没有,它会首先尝试从远程仓库下载构建至本地仓库,然后再使用本地仓库的构建。

默认的本地仓库路径为~/.m2/repository,要修改该默认位置,可在Mavensetting.xml文件中指定本地仓库路径:

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>/Library/maven/repository</localRepository>
</settings>

中央仓库(central

中央仓库是由Maven社区提供的仓库,需要通过网络进行访问。国内常用的是阿里云中央仓库。可在Mavensetting.xml文件中配置阿里云的中央仓库源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<localRepository>/Library/maven/repository</localRepository>
<mirrors>
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
</mirrors>
</settings>

Maven社区提供了一个网站:https://search.maven.org,可搜索到所有可以获取的构建库和JAR包。

远程仓库(remote

如果Maven在中央仓库也找不到依赖的构建,它会停止构建过程并输出错误信息到控制台。为了避免这种情况,Maven提供了远程仓库的概念,它是开发人员自己定制的仓库,包含了所需要的代码库或者其它工程中用到的JAR文件。

Maven依赖搜索顺序

首先从本地仓库(local)中搜索,如果找不到,则去中央仓库(central)中搜索;

如果在中央仓库(central)中找不到,则查看是否设置了远程仓库(remote),如果没有设置,则停止搜索并抛出错误(无法找到依赖)。

如果在远程仓库(remote)中找不到,则停止搜索并抛出错误(无法找到依赖);如果在远程仓库(remote)中找到了,则下载至本地仓库并引用。

Maven插件

Maven的三个标准生命周期中都包含一系列的阶段,每一个阶段的具体实现都是由Maven的插件完成。

例如我们使用的mvn clean命令,clean对应着Clean生命周期中的clean阶段,clean的具体操作是由maven-clean-plugin完成的。

插件类型

Maven提供了以下两种类型的插件:

  • Build Plugins:在构建时执行,并在pom.xml的元素中配置。
  • Reporting Plugins:在网站生成过程中执行,并在pom.xml的元素中配置。

常用插件

下面是一些常用的插件:

  • clean:构建之后清理目标文件。删除目标目录。
  • compiler:编译Java源文件。
  • surefile:运行Junit单元测试。创建测试报告。
  • jar:从当前工程中构建jar文件。
  • war:从当前工程中构建war文件。
  • javadoc:为工程生成javadoc
  • antrun:从构建过程的任意一个阶段中运行一个ant任务的集合。

引入外部依赖

有些时候我们需要去对接一些第三方的SDK包,一般第三方会提供SDK的下载地址,但是很可能未发布至中央仓库,就导致无法通过Maven坐标直接引入。这个时候我们需要手动将SDK包下载并复制粘贴到项目中,可在${basedir}/src/main/resources目录下新建一个lib目录,专门用来存放未发布至中央仓库的JAR包。引入方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<!-- 在这里添加你的依赖 -->
<dependency>
<!-- 库名称,可以自定义 -->
<groupId>xxx-sdk-1.0</groupId>
<!-- 库名称,可以自定义 -->
<artifactId>xxx</artifactId>
<!-- 版本号 -->
<version>1.0</version>
<!-- 作用域 -->
<scope>system</scope>
<!-- resource目录下的lib文件夹下,文件名为:xxx-sdk.jar -->
<systemPath>${basedir}\src\main\resources\lib\xxx-sdk.jar</systemPath>
</dependency>
</dependencies>

快照(SNAPSHOT

Apache Dubbo微服务项目开发中,服务消费方需要去引入服务提供方对外暴露的API接口jar包。服务提供方很有可能在短期内多次修改对外暴露的接口,如果按照正常的版本号:producer-service.jar:1.0.0,那每修改一次接口都需要升级一个小版本,服务消费方不得不去更新pom.xml来引用最新的jar包。如果修改接口而不升级版本号就发布至仓库中,服务消费方在引用时不会去仓库下载相同版本号的最新jar包。也就是说,服务消费方只会下载一次指定版本号的jar包。为了避免这种繁琐的更新,Maven提供了快照版本。

快照版本是一种特殊的版本,指定了当前的开发进度的副本。不同于常规的版本,Maven每次构建都会在远程仓库中检查新的快照,服务提供方只需发布包含最新代码的快照版本(producer-service.jar:1.0.0-SNAPSHOT)至仓库中,服务消费方每次都会自动去获取包含最新代码的快照(producer-service.jar:1.0.0-SNAPSHOT)。

依赖管理

Maven解析jar包的方式是依赖传递。例如我们引入spring-boot-starter-web,当Maven解析该依赖时,不仅仅会引入其内部依赖的spring-webspring-webmvcspring-boot-starter-tomcat等,还会引入这些内部依赖所依赖的jar包,例如spring-web依赖的spring-bean等,依赖关系不断向下传递,直至没有依赖,最终形成了一颗依赖树。

依赖冲突的问题

举个栗子:假设有依赖A,它内部的依赖传递关系为:A->B->C->D->E1;有另一个依赖F,它内部的依赖传递关系为:F->G->E2E1E2E的不同版本。

如果pom.xml同时引入AF依赖,按照Maven依赖传递原则,实际引入的依赖将包括:ABCDE1FGE2,因此E1E2将会产生包冲突。

解决依赖冲突

Maven解析pom.xml时,同一个groupIdartifactId的依赖只会保留一个,这样可以有效避免因引入不同版本的依赖所带来的问题。

Maven默认处理策略:

  • 最短路径优先原则:因为从FE2的路径比从AE1的路径短,所以Maven在面对E1E2时会选择E2
  • 最先声明优先原则:举个栗子:A->B->C1D->E->C2。这两个依赖传递的路径长度是一样的,所以谁在pom.xml先被声明就引入谁。

排除依赖:默认处理策略已经能解决包的依赖问题,但还是会显示依赖冲突,现象就是IDEA中会报红;同时默认处理引入的依赖版本号可能不是我们所需要的,如果我们想引入指定版本号的依赖,可以使用<exclusions /><exclusion>标签先排除冲突的依赖,再另外单独在pom.xml中引入指定版本号的依赖。

检测包冲突的Maven命令如下:

  • mvn dependency:help
  • mvn dependency:analyze
  • mvn dependency:tree
  • mvn dependency:tree -Dverbose

最佳实践

当开发多模块项目时,在最外层的父pom.xml中使用dependencyManagement标签进行依赖及版本号的预定义,便于统一管理项目依赖及版本号。在子模块引入依赖时,只需指定groupIdartifactId即可引入对应依赖。

参考

  • 菜鸟教程 - Maven教程