什么是Mavne
Maven 是一个项目管理工具,它包含了一个项目对象模型 (POM:Project Object Model)
,一组标准集合。由于 Maven 使用标准目录布局和默认构建生命周期,开发团队几乎可以立即自动化项目的构建基础设施。在多个开发团队环境的情况下,Maven 可以在很短的时间内按照标准设置工作方式。
Maven 之前,更多的是使用 Ant 的项目构建工具,Ant 有一个特点,每次都得写,每次都写的差不多,配置也臃肿。所以,后来搞出来 Maven。Maven 就是最先进的版本构建工具吗?不是的,只不过,目前在 Java 领域 Maven 使用比较多。除了 Maven,还有 Gradle。
它的主要功能有:
- 提供了一套标准化的项目结构;
- 提供了一套标准化的构建流程(编译,测试,打包,发布……);
- 提供了一套依赖管理机制。
为了实现上面的主要功能,Maven提供了两大核心:
- 依赖管理:对 jar 的统一管理(Maven 提供了一个 Maven 的中央仓库,当我们在项目中添加完会自动去中央仓库下载相关的依赖,并且解决依赖的依赖问题)
- **项目构建:**对项目进行编译、测试、打包、部署、上传到私服等
Maven模型
下面来谈一谈Maven模型的整体结构,包括三个部分:
- 项目对象模型 (Project Object Model)
- 依赖管理模型(Dependency)
- 插件(Plugin)
如上图所示,包括蓝、黄两个部分分别对应着依赖关系和项目构建两大核心功能。
首当其冲的一个核心就是项目对象模型,也就是经常使用的pom.xml
另外一个就是项目构建,Maven的项目构建可以按照生命周期具备以下三个标准生命周期:
- clean:项目清理的处理
- default(或 build):项目部署的处理
- site:项目站点文档创建的处理
项目对象模型(POM)
POM 代表项目对象模型。它是 Maven 中的基本工作单元。它是一个 XML 文件,作为 pom.xml 驻留在项目的跟目录中。POM 不仅包含有关项目的信息以及 Maven 用于构建项目的各种配置详细信息, 还包含目标和插件。
在执行任务或目标时,Maven 在当前目录中查找 POM。它读取 POM,获取所需的配置信息,然后执行目标。在POM文件中常见的配置包括一下几点:
- 项目依赖
- 插件
- 目标
- 建立档案
- 项目版本
- 开发商
- 邮件列表
在创建 POM 之前,我们应该首先确定项目组(groupId)、项目名称(artifactId) 和版本,因为这些属性有助于在存储库中唯一标识项目。
<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.coderV</groupId>
<artifactId>coderV</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>com.coderV</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
//...
</plugin>
</plugins>
</build>
</project>
复制代码
接下来仔细拆解POM文件结构
1、项目标识符
Maven 使用一组标识符(也称为坐标)来唯一标识项目并指定应如何打包项目工件:
- groupId: 创建项目的公司或组的唯一基本名称
- artifactId: 项目的唯一名称
- **version:**项目的一个版本
- packaging: 一种打包方法(例如WAR / JAR / ZIP)
其中所有 POM 文件都需要项目元素和三个必填字段:groupId、artifactId、version。
其中的前三个 ( groupId:artifactId:version ) 结合形成唯一标识符,并且是您指定项目将使用的外部库(例如 JAR)版本的机制。
详细说明一些基本的项目标识符6
参数名称 | 描述 |
---|---|
Project root | 这是项目root的标签。您需要指定基本架构设置,例如 apache 架构和 w3.org 规范。 |
Model version | 模型版本应为 4.0.0。 |
**groupId ** | 这是项目组/公司的 ID。这在组织或项目中通常是唯一的。 |
artifactId | 这是项目的 ID。这通常是项目的名称 |
version | 这是项目的版本 |
2、依赖管理
Maven 使用存储库的概念进行依赖管理,项目中使用的这些外部库称为依赖项。Maven 中的依赖项管理功能可确保从中央存储库自动下载这些库,因此您不必将它们存储在本地。
这是 Maven 的一个关键特性,并提供以下好处:
- 结合本地仓库大幅减少从远程存储库下载的数量,减少存储使用量
- 管理项目依赖性变得更容易
- 提供了一个有效的平台,用于在组织内外交换二进制组件,而无需每次都从源代码手动安装组件
为了声明对外部库的依赖,您需要提供库的groupId、artifactId、verison。让我们看一个例子:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.16</version>
</dependency>
复制代码
当 Maven 处理依赖项时,它会将 Spring Core 库下载到本地 Maven 存储库中,并在项目中使用
传递依赖
如下图所示,项目 A 依赖于项目 B,B 又依赖于项目 C,此时 B 是 A 的直接依赖,C 是 A 的间接依赖
Maven 的依赖传递机制是指:不管 Maven 项目存在多少间接依赖,POM 中都只需要定义其直接依赖,不必定义任何间接依赖,Maven 会动读取当前项目各个直接依赖的 POM,将那些必要的间接依赖以传递性依赖的形式引入到当前项目中。Maven 的依赖传递机制能够帮助用户一定程度上简化 POM 的配置。
基于 A、B、C 三者的依赖关系,根据 Maven 的依赖传递机制,我们只需要在项目 A 的 POM 中定义其直接依赖 B,在项目 B 的 POM 中定义其直接依赖 C,Maven 会解析 A 的直接依赖 B的 POM ,将间接依赖 C 以传递性依赖的形式引入到项目 A 中。
通过这种依赖传递关系,可以使依赖关系树迅速增长到一个很大的量级,很有可能会出现依赖重复,依赖冲突等情况,Maven 针对这些情况提供了如下功能进行处理。
- 依赖范围(Dependency scope)
- 依赖调解(Dependency mediation)
- 可选依赖(Optional dependencies)
- 排除依赖(Excluded dependencies)
- 依赖管理(Dependency management)
Maven 具有以下 6 中常见的依赖范围,如下表所示。
依赖范围 | 描述 |
---|---|
compile | 编译依赖范围,scope 元素的缺省值。使用此依赖范围的 Maven 依赖,对于三种 classpath 均有效,即该 Maven 依赖在上述三种 classpath 均会被引入。例如,log4j 在编译、测试、运行过程都是必须的。 |
test | 测试依赖范围。使用此依赖范围的 Maven 依赖,只对测试 classpath 有效。例如,Junit 依赖只有在测试阶段才需要。 |
provided | 已提供依赖范围。使用此依赖范围的 Maven 依赖,只对编译 classpath 和测试 classpath 有效。例如,servlet-api 依赖对于编译、测试阶段而言是需要的,但是运行阶段,由于外部容器已经提供,故不需要 Maven 重复引入该依赖。 |
runtime | 运行时依赖范围。使用此依赖范围的 Maven 依赖,只对测试 classpath、运行 classpath 有效。例如,JDBC 驱动实现依赖,其在编译时只需 JDK 提供的 JDBC 接口即可,只有测试、运行阶段才需要实现了 JDBC 接口的驱动。 |
system | 系统依赖范围,其效果与 provided 的依赖范围一致。其用于添加非 Maven 仓库的本地依赖,通过依赖元素 dependency 中的 systemPath 元素指定本地依赖的路径。鉴于使用其会导致项目的可移植性降低,一般不推荐使用。 |
import | 导入依赖范围,该依赖范围只能与 dependencyManagement 元素配合使用,其功能是将目标 pom.xml 文件中 dependencyManagement 的配置导入合并到当前 pom.xml 的 dependencyManagement 中。 |
依赖范围与三种 classpath 的关系一览表,如下所示。
依赖范围 | 编译 classpath | 测试 classpath | 运行 classpath | 例子 |
---|---|---|---|---|
compile | √ | √ | √ | log4j |
test | - | √ | - | junit |
provided | √ | √ | - | servlet-api |
runtime | - | - | √ | JDBC-driver |
system | √ | √ | - | 非 Maven 仓库的本地依赖 |
依赖范围对传递依赖的影响
项目 A 依赖于项目 B,B 又依赖于项目 C,此时我们可以将 A 对于 B 的依赖称之为第一直接依赖,B 对于 C 的依赖称之为第二直接依赖。
B 是 A 的直接依赖,C 是 A 的间接依赖,根据 Maven 的依赖传递机制,间接依赖 C 会以传递性依赖的形式引入到 A 中,但这种引入并不是无条件的,它会受到依赖范围的影响。
传递性依赖的依赖范围受第一直接依赖和第二直接依赖的范围影响,如下表所示。
compile | test | provided | runtime | |
---|---|---|---|---|
compile | compile | - | - | runtime |
test | test | - | - | test |
provided | provided | - | provided | provided |
runtime | runtime | - | - | runtime |
注:上表中,左边第一列表示第一直接依赖的依赖范围,上边第一行表示第二直接依赖的依赖范围。交叉部分的单元格的取值为传递性依赖的依赖范围,若交叉单元格取值为“-”,则表示该传递性依赖不能被传递。
通过上表,可以总结出以下规律(*):
- 当第二直接依赖的范围是 compile 时,传递性依赖的范围与第一直接依赖的范围一致;
- 当第二直接依赖的范围是 test 时,传递性依赖不会被传递;
- 当第二直接依赖的范围是 provided 时,只传递第一直接依赖的范围也为 provided 的依赖,且传递性依赖的范围也为 provided;
- 当第二直接依赖的范围是 runtime 时,传递性依赖的范围与第一直接依赖的范围一致,但 compile 例外,此时传递性依赖的范围为 runtime。
依赖调节
Maven 的依赖传递机制可以简化依赖的声明,用户只需要关心项目的直接依赖,而不必关心这些直接依赖会引入哪些间接依赖了。为了避免出现依赖重复的问题,Maven 通过依赖调节来确定间接依赖的引入路径。
依赖调节遵循以下两条原则:
- 引入路径短者优先
- 先声明者优先
引入路径短者优先
引入路径短者优先,顾名思义,当一个间接依赖存在多条引入路径时,引入路径短的会被解析使用。
例如,A 存在这样的依赖关系: A->B->C->D(1.0) A->X->D(2.0)
D 是 A 的间接依赖,但两条引入路径上有两个不同的版本,很显然不能同时引入,否则造成重复依赖的问题。根据 Maven 依赖调节的第一个原则:引入路径短者优先,D(1.0)的路径长度为 3,D(2.0)的路径长度为 2,因此间接依赖 D(2.0)将从 A->X->D(2.0) 路径引入到 A 中。
先声明者优先
先声明者优先,顾名思义,在引入路径长度相同的前提下,POM 文件中依赖声明的顺序决定了间接依赖会不会被解析使用,顺序靠前的优先使用。
例如,A 存在以下依赖关系: A->B->D(1.0) A->X->D(2.0)
D 是 A 的间接依赖,其两条引入路径的长度都是 2,此时 Maven 依赖调节的第一原则已经无法解决,需要使用第二原则:先声明者优先。
Maven排除依赖和可选依赖
我们知道 Maven 依赖具有传递性,例如 A 依赖于 B,B 依赖于 C,在不考虑依赖范围等因素的情况下,Maven 会根据依赖传递机制,将间接依赖 C 引入到 A 中。但如果 A 出于某种原因,希望将间接依赖 C 排除,那该怎么办呢?Maven 为用户提供了两种解决方式:排除依赖(Dependency Exclusions)和可选依赖(Optional Dependencies)。
排除依赖
假设存在这样的依赖关系,A 依赖于 B,B 依赖于 X,B 又依赖于 Y。B 实现了两个特性,其中一个特性依赖于 X,另一个特性依赖于 Y,且两个特性是互斥的关系,用户无法同时使用两个特性,所以 A 需要排除 X,此时就可以在 A 中将间接依赖 X 排除。
排除依赖是通过在 A 中使用 exclusions 元素实现的,该元素下可以包含若干个 exclusion 子元素,用于排除若干个间接依赖,示例代码如下:
<dependencies>
<dependency>
<groupId>net.biancheng.www</groupId>
<artifactId>B</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<!-- 设置排除 -->
<!-- 排除依赖必须基于直接依赖中的间接依赖设置为可以依赖为 false -->
<!-- 设置当前依赖中是否使用间接依赖 -->
<exclusion>
<!--设置具体排除-->
<groupId>net.biancheng.www</groupId>
<artifactId>X</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
复制代码
关于 exclusions 元素及排除依赖说明如下:
- 排除依赖是控制当前项目是否使用其直接依赖传递下来的间接依赖;
- exclusions 元素下可以包含若干个 exclusion 子元素,用于排除若干个间接依赖;
- exclusion 元素用来设置具体排除的间接依赖,该元素包含两个子元素:groupId 和 artifactId,用来确定需要排除的间接依赖的坐标信息;
- exclusion 元素中只需要设置 groupId 和 artifactId 就可以确定需要排除的依赖,无需指定版本 version。
可选依赖
与上文的应用场景相同,也是 A 希望排除间接依赖 X,除了在 B 中设置可选依赖外,我们还可以在 B 中将 X 设置为可选依赖。
设置可选依赖
在 B 的 POM 关于 X 的依赖声明中使用 optional 元素,将其设置成可选依赖,示例配置如下:
<dependencies>
<dependency>
<groupId>net.biancheng.www</groupId>
<artifactId>X</artifactId>
<version>1.0-SNAPSHOT</version>
<!--设置可选依赖 -->
<optional>true</optional>
</dependency>
</dependencies>
复制代码
关于 optional 元素及可选依赖说明如下:
- 可选依赖用来控制当前依赖是否向下传递成为间接依赖;
- optional 默认值为 false,表示可以向下传递称为间接依赖;
- 若 optional 元素取值为 true,则表示当前依赖不能向下传递成为间接依赖。
排除依赖 VS 可选依赖
排除依赖和可选依赖都能在项目中将间接依赖排除在外,但两者实现机制却完全不一样。
-
- 排除依赖是控制当前项目是否使用其直接依赖传递下来的接间依赖;
- 可选依赖是控制当前项目的依赖是否向下传递;
- 可选依赖的优先级高于排除依赖;
- 若对于同一个间接依赖同时使用排除依赖和可选依赖进行设置,那么可选依赖的取值必须为 false,否则排除依赖无法生效。
3、Maven 仓库
在 Maven 术语中,仓库是存储所有项目的 jar包的地方,Maven 可以轻松使用它们。根据类别可以将Maven仓库分为三类:
1)本地仓库 默认情况下,每个本地计算机的用户目录下都有一个路径名为.m2/repository/的仓库目录,这个就是本地的仓库
也可以在 settings.xml 文件配置本地仓库的路径
2)远程仓库 远程仓库也称为私服,由公司或者项目组维护,是开发人员自己定义的远程仓库,其中包含项目所需要的库或其他的jar包,可以通过标签来制定远程仓库地址
<repositories>
<repository>
<id>companyname.lib1</id>
<url>http://download.companyname.org/maven2/lib1</url>
</repository>
</repositories>
复制代码
3)中央仓库
Maven中央仓库是Maven社区提供的仓库。它包含大量常用的库。当 Maven 在本地存储库中找不到任何依赖项时,就会从中央仓库中搜索
其中中央仓库有以下几点需要注意:
- 此存储库由 Maven 社区管理。
- 不需要配置,但可以替换中央仓库的源为制定的镜像源
- 它需要互联网访问才能搜索。
Maven 依赖加载顺序
当我们执行 Maven 构建命令时,Maven 开始按以下顺序查找依赖库:
- 第 1 步- 在本地存储库中搜索依赖项,如果未找到,则转到第 2 步,否则执行进一步处理。
- 第 2 步- 如果没有提到远程存储库,则跳转到第 3 步。在一个或多个远程存储库中搜索依赖项,如果找到则将其下载到本地存储库以供将来参考
- 第 3 步- 在中央存储库中搜索依赖项,将其下载到本地存储库以供将来参考。如果未找到 Maven 只是停止处理并抛出错误(无法找到依赖项)
4、属性(properties)
自定义属性有助于pom.xml
文件更易于阅读和维护。一个十分经典的使用场景就是,通过自定义属性来定义项目依赖项的版本。
Maven的properties
是一个占位符,通过properties
定义不同属性名的值
例如下面的例子中,通过<properties>
定义了一个spring.version
的属性,其具体的值为 5.3.16
;如果想要将 Spring 升级到更新的版本,就只需更改<spring.version>
属性标签内的值,所有在其<version>
标签中使用该属性的依赖项都将更新。
<properties>
<spring.version>5.3.16</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
复制代码
另一个常用的场景就是使用<properties>
定义构建路径的变量,例如
<properties>
<project.build.folder>${project.build.directory}/tmp/</project.build.folder>
</properties>
<plugin>
//...
<outputDirectory>${project.resources.build.folder}</outputDirectory>
//...
</plugin>
复制代码
5、Build
build
部分也是 Maven POM 中非常重要的部分。它提供有关默认 Maven目标、已编译项目的目录和应用程序的最终名称的信息。默认build
如下所示:
<build>
<!--当项目没有规定目标(Maven2叫做阶段(phase))时的默认值, -->
<!--必须跟命令行上的参数相同例如jar:jar,或者与某个阶段(phase)相同例如install、compile等 -->
<defaultGoal>install</defaultGoal>
<!-- 构建产生的所有文件存放的目录,默认为${basedir}/target,即项目根目录下的target -->
<directory>${basedir}/target</directory>
<!-- 产生的构件的文件名,默认值是${artifactId}-${version}-->
<finalName>${artifactId}-${version}</finalName>
<!--当filtering开关打开时,使用到的过滤器属性文件列表。 -->
<!--项目配置信息中诸如${spring.version}之类的占位符会被属性文件中的实际值替换掉 -->
<filters>
<filter>filters/filter1.properties</filter>
</filters>
//...
</build>
复制代码
编译工件的默认输出文件夹名为*target*,打包工件的最终名称由*artifactId*和*version*组成,但您可以随时更改。
6、配置文件
构建配置文件是一组配置值,可用于设置或覆盖 Maven 构建的默认值。使用构建配置文件,您可以为不同的环境(例如生产环境和开发环境)自定义构建。
配置文件主要分为两种类型,一种是在定义在项目上pom.xml文件中,另一种是定义在setting.xml上
<profiles>
<profile>
<id>production</id>
<build>
<plugins>
<plugin>
//...
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>development</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
//...
</plugin>
</plugins>
</build>
</profile>
</profiles>
复制代码
定义在pom.xml文件上的profile可以看做pom.xml的副本,拥有与pom.xml相同的子元素与配置方法。
正如您在上面的示例中看到的,默认配置文件设置为development。如果要运行生产配置文件,可以使用以下 Maven 的 -P 命令显示的激活一个profile:
mvn clean install -P production
复制代码
Maven 构建生命周期
每个 Maven 构建都遵循指定的生命周期。您可以执行多个构建生命周期目标,包括编译项目代码、创建包以及在本地 Maven 依赖项存储库中安装存档文件的目标。
以下列表显示了最重要的 Maven生命周期阶段:
- validate: 检查项目的正确性
- compile: 将提供的源代码编译成二进制工件
- test: 执行单元测试
- package: 将编译后的代码打包*到归档文件中
- integration-test: 执行额外的测试,这需要打包
- verify——检查包是否有效
- install – 将包文件安装到本地 Maven 存储库
- deploy – 将包文件部署到远程服务器或存储库
1、Clean 生命周期
当我们执行 mvn post-clean 命令时,Maven 调用 clean 生命周期,它包含以下阶段:
- pre-clean:执行一些需要在clean之前完成的工作
- clean:移除所有上一次构建生成的文件
- post-clean:执行一些需要在clean之后立刻完成的工作
在一个生命周期中,运行某个阶段的时候,它之前的所有阶段都会被运行,也就是说,如果执行 mvn clean 将运行pre-clean, clean两个生命周期阶段;运行 mvn post-clean ,则运行pre-clean, clean, post-clean三个生命周期阶段
2、Default (Build) 生命周期
这是 Maven 的主要生命周期,被用于构建应用,包括下面的 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-classes(处理测试类文件) | 处理测试源码编译生成的文件。 |
test(测试) | 使用合适的单元测试框架运行测试(Juint是其中之一)。 |
prepare-package(准备打包) | 在实际打包之前,执行任何的必要的操作为打包做准备。 |
package(打包) | 将编译后的代码打包成可分发格式的文件,比如JAR、WAR或者EAR文件。 |
pre-integration-test(集成测试前) | 在执行集成测试前进行必要的动作。比如说,搭建需要的环境。 |
integration-test(集成测试) | 处理和部署项目到可以运行集成测试环境中。 |
post-integration-test(集成测试后) | 在执行集成测试完成后进行必要的动作。比如说,清理集成测试环境。 |
verify (验证) | 运行任意的检查来验证项目包有效且达到质量标准。 |
install(安装) | 安装项目包到本地仓库,这样项目包可以用作其他本地项目的依赖。 |
deploy(部署) | 将最终的项目包复制到远程仓库中与其他开发者和项目共享。 |
Site 生命周期
Maven Site 插件一般用来创建新的报告文档、部署站点等。
- pre-site:执行一些需要在生成站点文档之前完成的工作
- site:生成项目的站点文档
- post-site: 执行一些需要在生成站点文档之后完成的工作,并且为部署做准备
- site-deploy:将生成的站点文档部署到特定的服务器上
插件(Plugin)
插件管理与依赖管理原理一样、不同的是定义的元素标签不一样、插件管理标签是build标签的子标签pluginManagement
中
pluginManagement
用来做插件管理的。它是表示插件声明,即你在项目中的pluginManagement
下声明了插件,Maven不会加载该插件,pluginManagement
声明可以被继承。如下面的例子
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<!-- 子POM-->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
</plugin>
</plugins>