1. Maven简介
1.1. 为什么使用Maven
- 项目中手动管理jar包和jar包之间的依赖关系很麻烦;
- 项目不断的变大,导致维护变得麻烦;
- 项目的构建过程中存在大量重复步骤;
1.2. Maven是什么
Maven 翻译为"专家"、"内行",是 Apache 下的一个纯 Java 开发的开源项目。基于项目对象模型(缩写:POM)概念,Maven利用一个中央信息片断能管理一个项目的构建、报告和文档等步骤。
Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。
Maven 也可被用于构建和管理各种项目,例如 C#,Ruby,Scala 和其他语言编写的项目。Maven 曾是 Jakarta 项目的子项目,现为由 Apache 软件基金会主持的独立 Apache 项目。Maven官网 和 Maven官网中文翻译
Maven解决的问题:
- 项目jar包管理
- 项目自动化构建
- 项目模块化管理
Maven解决以上问题的方式:
jar包管理:通过在POM对象中书写坐标引用的方式确定jar包的版本信息;
自动化构建:通过Maven定义生命周期,通过插件的调用实现自动化项目构建;
项目模块化管理:通过定义父级Maven项目,配置子项目模块的方式进行管理;
1.3. 配置Maven
关于Maven的配置网上有很多的教程,此处不再赘述,这里放一个配置教程:IDEA和Eclipse中配置Maven
2. Maven核心概念
2.1. 约定的目录结构
当我们使用Maven进行自动编译时,Maven需要首先找到Java源文件,下一步才能进行自动编译,自动编译后的字节码文件也需要存放在准确的位置,我们可以采用两种方式让开发工具IDE和框架知道我们的项目资源文件位置:
① 通过配置的形式明确指明文件位置
② 通过三方工具和框架的约定
JavaEE开发领域普遍认同的观点,约定 > 配置 > 编码。基本意思是,能通过配置解决的问题就不必要进行编码,能基于约定解决的问题就不进行配置。Maven就是通过约定项目目录的方式对我们的Maven项目进行自动化构建。
约定的项目目录结构对于Maven的自动化构建而言很重要,Maven可以到约定的目录结构下查找到项目的资源文件,测试文件,对项目进行自动化构建。如果不按照约定的目录结构创建的项目会导致Maven无法识别项目内容,也就没办法进行自动化项目构建。
Maven中约定的项目目录结构:
demo 项目文件目录
|---src 项目的源文件目录
|---|---main 项目主文件目录
|---|---|---java 项目java文件目录
|---|---|---resources 项目资源文件目录
|---|---test 测试文件目录
|---|---|---java 测试java文件目录
|---|---|---resources 测试资源文件目录
|---target 打包后的项目输出目录(此目录会在执行Maven打包命令后生成)
|---|---classes 编译输出目录
|---|---test-classes 测试编译输出目录
|---pom.xml Maven构建文件
2.2.POM
POM:Project Object Model(项目对象模型)是 Maven 工程的基本工作单元,是一个XML文件,也就是我们定义在项目路径下的 pom.xml 文件,其中包含了项目的基本信息,用于描述项目如何构建,项目中所需的依赖,等等。
Maven在执行任务或目标时会首先在当前的项目目录中查找POM,并读取POM中的配置,依照其中的配置对项目进行构建。
典型的POM文件:
<?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>cn.bruce</groupId>
<!-- 项目的唯一ID,一个groupId下可能有多个项目,通过artifactId进行区分 -->
<artifactId>maven-demo</artifactId>
<!-- 此项目的版本号 -->
<version>1.0-SNAPSHOT</version>
</project>
我们可以通过配置POM文件实现对工程的管理,POM文件中还有很多可以配置的地方,下面内容中还会提及到。
完整的POM文件信息:Maven POM
2.3. 坐标
和数学中使用 (x,y,z) 坐标来表示三维空间内的某一点相似,Maven使用三个向量(GAV模型)来定位仓库中的一个maven工程,这三个向量分别是:
- gropId:组织id
- artifactId:模块名
- version:版本
在POM文件中使用 <dependency></dependency> 标签来描述项目中jar包依赖关系
示例:
<dependencies>
<dependency>
<!-- 依赖的组织名 -->
<groupId>mysql</groupId>
<!-- 依赖的模块名 -->
<artifactId>mysql-connector-java</artifactId>
<!-- 依赖的版本 -->
<version>8.0.17</version>
</dependency>
......
</dependencies>
版本中常见的单词:
- SNAPSHOT:快照版本,不稳定的版本
- RELEASE/GA:稳定版本
坐标和本地仓库中jar包的对应关系:
pom文件中的坐标:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.4.RELEASE</version>
</dependency>
......
</dependencies>
实际仓库中的路径:
Maven工程不再将jar包放在项目目录中,而是在POM中指出仓库中的引用位置,具体的依赖的jar包存放在本地仓库中。
2.4. 依赖
依赖的作用:管理项目中需要用到的jar包,换句话说依赖就是项目中使用的jar包,我们使用Maven最主要的就是使用它的依赖管理功能。针对依赖的管理就是对项目中的jar包进行管理,也可以管理依赖的使用范围,依赖之间的传递关系,统一依赖的版本等问题。我们使用前面提及到的坐标功能来导入项目中的jar包,如:在项目中导入MySQL的驱动。
<?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>cn.bruce</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
</dependency>
</dependencies>
</project>
2.4.1. 依赖的范围:
依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系,Maven有以下几种依赖范围。
complie:编译依赖范围。如果没有指定,就会默认使用该依赖范围。使用此依赖范围的maven依赖,对于编译 测试 运行三种的classpath都有效。test:测试依赖范围。使用此依赖范围的Maven依赖,只对于测试的classpath有效,在编译主代码或者运行主代码的时候都无法依赖此类依赖。典型的例子是jUnit,它只有在编译测试代码及运行测试代码的时候才有效。provided:已提供依赖范围。使用此依赖范围的maven依赖,对于编译和测试classpath有效,但在运行时无效。典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行的时候,由于容器已经提供,就不需要maven重复地引入一遍。打包的时候可以不用包进去,别的设施会提供。事实上该依赖理论上可以参与编译,测试,运行等周期。相当于compile,但是打包阶段做了exclude操作。runtime:运行时依赖范围。使用此依赖范围的maven依赖,对于测试和运行classpath有效,但在编译主代码时无效。典型的例子是JDBC驱动实现,项目主代码的编译只需要jdk提供的jdbc的接口,只有在执行测试或者运行测试的时候才需要实现上述接口的jdbc的驱动system:系统依赖范围。从参与度来说,和provided相同,不过被依赖项不会从maven仓库下载,而是从本地文件系统拿。需要添加systemPath的属性来定义路径,该依赖与三种范围的classpath和provided依赖范围完全一致。可能造成不可移植,谨慎使用。import:导入依赖范围。该依赖范围不会对三种classpath产生实际的影响。只有在dependencyManagement下才有效果。
常用的声明周期范围对比:
| complie | test | provided | |
|---|---|---|---|
| 是否对主程序生效 | 是 | 否 | 是 |
| 是否参与测试阶段 | 是 | 是 | 是 |
| 是否参与部署 | 是 | 否 | 否 |
在POM文件中使用 <scop></scop> 标签设置jar包的依赖范围,示例:
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<!-- 在测试阶段生效 -->
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<!-- 在部署时排除此依赖 -->
<scope>provided</scope>
</dependency>
</dependencies>
2.4.2. 依赖的传递
依赖具有传递性,我们导入的jar包依赖其他jar包,Maven会将jar包所依赖的jar包同时传递过来。
例如:项目中依赖spring-boot-stater:2.1.8.RELEASE jar包,但是此jar包同时依赖其他的jar包,我们在引入此依赖的同时Maven会将 spring-boot-stater:2.1.8.RELEASE 所依赖的jar包也传递到项目中,通过IDEA提供的Maven管理工具我们可以看到如下图所示:
好处:可传递的依赖不必要在每个项目中书写,在公共项目中配置好,其他项目依赖即可。
例如:我们在 maven-demo 项目中配置好所需的依赖,在 maven-model-1 项目中引入 maven-demo 依赖,即可在 maven-model-1 项目中使用 maven-demo 所依赖的jar包。
maven-demo 项目的POM结构
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
</dependencies>
maven-model-1 项目的POM结构:
<dependencies>
<dependency>
<groupId>cn.bruce</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
通过IDEA的Maven管理工具可以查看到 maven-model-1 项目中已经导入了 maven-demo 项目所需的依赖。
我们注意到
maven-demo中有一些依赖并没有被传递到maven-model-1中,这是因为非compile范围依赖不能被传递。
2.4.3. 依赖的排除
我们在当前的工程中引入了依赖A,而A又依赖了B,此时Maven会将A和B同时引入当前工程,但是我们在工程中已经引入了B,此时我们希望A直接依赖项目中的B而不是原本依赖的B,因此我们需要对A中的B进行依赖排除。
依赖排除的主要目的也是为了对项目中的依赖版本进行统一的管理,在POM文件中 <exclusion></exclusion> 标签进行依赖排除操作。
示例代码:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</exclusion>
</exclusions>
</dependency>
未排除前:
排除后:
2.4.4. 依赖的原则
我们可以通过上面的依赖排除解决工程中依赖冲突的问题,同时Maven也提供了解决依赖冲突的方案,即我们不解决冲突问题,Maven也是会帮我们解决的,但是不建议这样,出现依赖冲突最好还是解决避免造成项目的不稳定。
Maven解决冲突的原则:
① 路径最短优先
如图所示,项目A依赖项目B,项目B同时依赖项目C,且项目A、项目B、项目C同时依赖 slf4j-api 但是依赖的版本不同,项目A,B,C对 slf4j-api 的依赖路径分别为1,2,3。根据路径最短原则,项目A中最终引入的 slf4j-api 的版本为 1.7.30
路径:当前项目和依赖的maven工程之间的模块层级。
② 先声明者优先
项目A中的POM文件:
<dependencies>
<dependency>
<groupId>cn.bruce</groupId>
<artifactId>projectB</artifactId>
<version>1.0.1.SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.bruce</groupId>
<artifactId>projectC</artifactId>
<version>1.0.1.SNAPSHOT</version>
</dependency>
</dependencies>
如图所示项目,A同时依赖项目B和项目C,且项目A、项目B、项目C同时依赖 slf4j-api 但是依赖的版本不同,此时项目中依赖的 slf4j-api 的版本取决于项目A中,对项目B和项目C依赖的声明位置。在项目A的POM文件中,projectB的声明在projectC的声明前,因此项目中使用projectB中指定的 slf4j-api:1.7.23
先声明指的是
<dependency></dependency>标签声明的顺序
2.4.5. 统一管理依赖的版本
我们在项目中可能依赖同一个框架的一组jar包,我们在使用的时候,最好使用统一的版本,如果采用我们上面讲到的内容,我们需要对框架内的每个依赖包逐一进行版本的修改和设置,Maven提供了让我们统一修改版本
项目中的POM文件
<properties>
<spring.version>5.0.0</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-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
此时项目中Spring各个版本jar包依赖都是5.0.0,如果想统一升级为5.0.1时,仅需修改 <properties> 标签中的版本号即可。因此建议项目中依赖jar包使用 <properties>自定义标签名</properties> 标签进行统一的版本管理,在需要用的的地方使用 ${自定义标签名}
<properties>自定义标签名</properties> 标签不仅仅用来进行版本号管理,凡是需要统一声明后再引用的场合都可以使用此标签。如:声明项目的编译编码和Java的版本
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
2.4.6. 查找Maven工程版本
当我们在POM文件中指定好依赖的相关信息(即定位到指定的jar包版本后)Maven的查找顺序:
① Maven会首先到我们的本地仓库中查找jar包;
② 本地仓库中查找不到会到Maven私服中查找;
③ Maven私服中还未查到则会到远程仓库中查找jar包,找到后下载到本地;
⑤ 下次在再使用时直接引用本地仓库中的jar包;
查找过程:本地-->私服--> 远程仓库
2.5. 仓库
2.5.1. 仓库的分类
① 本地仓库:本机中的仓库,为本机上的所有Maven工程服务
② 远程仓库
- Maven私服:部署在局域网环境下,为当前局域网范围内的Maven工程服务;
- Maven中央仓库:部署再Internet上,为全世界的Maven工程服务;
- Maven中央仓库镜像:为中央仓库分担压力,提供更快的依赖请求服务,如:阿里云镜像仓库
2.5.2. 仓库中的文件
仓库中的内容:Maven工程
- Maven自身所需的插件
- 第三方框架或工具jar包
- 我们自己开发的Maven工程
2.6. 生命周期/插件/目标
2.6.1. 生命周期
Maven的生命周期是指Maven在执行项目构建时的各个环节,Maven的生命周期定义了各个构建环节的执行顺序,有了执行顺序清单,Maven就可以自动化的执行构建命令。
Maven包含三套生命周期:
① Clean Lifecycle 在进行真正的项目构建之前进行清理工作;
② Default Lifecyle 构建的核心部分,编译、测试、打包、安装、部署等等;
③ Site Lifecycle 生成项目报告、站点、发布站点;
2.6.1.1 Clean 生命周期
| 阶段 | 描述 |
|---|---|
pre-clean | 在实际项目清理之前执行所需的过程 |
clean | 删除上一版本生成的所有文件 |
post-clean | 执行完成项目清理所需的过程 |
6.1.2 Default 生命周期
Default生命周期是Maven生命周期中最重要的一个,绝大部分工作都发生在这个生命周期中。
| 阶段 | 描述 |
|---|---|
validate | 验证项目正确无误,并提供所有必要的信息。 |
initialize | 初始化构建状态,例如设置属性或创建目录。 |
generate-sources | 生成任何要包含在编译中的源代码。 |
process-sources | 处理源代码,例如过滤任何值。 |
generate-resources | 生成资源以包含在包中。 |
process-resources | 将资源复制并处理到目标目录中,以备打包。 |
compile | 编译项目的源代码。 |
process-classes | 对编译后生成的文件进行后处理,例如对Java类进行字节码增强。 |
generate-test-sources | 生成任何测试源代码以包含在编译中。 |
process-test-sources | 处理测试源代码,例如过滤所有值。 |
generate-test-resources | 创建测试资源。 |
process-test-resources | 将资源复制并处理到测试目标目录中。 |
test-compile | 将测试源代码编译到测试目标目录中 |
process-test-classes | 从测试编译中对生成的文件进行后处理,例如对Java类进行字节码增强。 |
test | 使用合适的单元测试框架运行测试。这些测试不应要求将代码打包或部署。 |
prepare-package | 在实际包装之前执行准备包装所需的任何操作。这通常会导致包装的未包装,已处理版本。 |
package | 获取编译后的代码,并将其打包为可分发格式,例如JAR。 |
pre-integration-test | 在执行集成测试之前执行所需的操作。这可能涉及诸如设置所需环境的事情。 |
integration-test | 处理该程序包并将其部署到可以运行集成测试的环境中(如有必要)。 |
post-integration-test | 在执行集成测试后执行所需的操作。这可能包括清理环境。 |
verify | 运行任何检查以确认包装有效并符合质量标准。 |
install | 将软件包安装到本地存储库中,以作为本地其他项目中的依赖项。 |
deploy | 在集成或发布环境中完成后,将最终程序包复制到远程存储库,以便与其他开发人员和项目共享。 |
6.1.3 Site 生命周期
一般用于创建报告文档、部署站点等。
| 阶段 | 描述 |
|---|---|
pre-site | 在实际项目站点生成之前执行所需的过程 |
site | 生成项目的站点文档 |
post-site | 执行完成站点生成并为站点部署做准备所需的过程 |
site-deploy | 将生成的站点文档部署到指定的Web服务器 |
Maven构建生命周期的特点:
- 各个构建环节的执行顺序:不能打乱顺序,必须按照正确的顺序执行;
- maven核心程序定义了抽象的生命周期,生命周期中的具体任务由插件完成;
- Maven会按照顺序执行,执行生命周期中的任何阶段,都会从最初的位置开始执行;
2.6.2. 插件和插件目标
maven的核心程序中仅仅定义了抽象的生命周期,具体的工作需要通过特定的插件完成,插件本身并不包含在Maven核心程序中。当项目构建时,maven会首先到本地仓库中查找插件,找不到会到远程仓库中下载 。
每个插件都能实现多个功能,每个功能就是一个插件目标;
Maven生命周期与插件的目标相互绑定,来完成某个具体的构建任务;
插件目标:
- 生命周期的各个阶段仅仅定义了要执行什么任务
- 各个阶段和插件的目标是对应的
- 相似的目标由插件完成
- 可以将目标看作调用功能的命令
| 生命周期阶段 | 插件目标 | 插件 |
|---|---|---|
| compile | compile | maven-compiler-plugin |
| test-compile | test-compile | maven-compiler-plugin |
| ...... | ...... | ...... |
2.7. 继承
我们想对项目中的junit依赖版本进行统一的管理,由于test范围的依赖不能传递,容易造成版本不一致。Maven为我们提供了继承的方式来解决这些问题。
解决方式:在父工程中指定junit的完整依赖信息,子工程中声明junit依赖时不指定版本,以父工程中统一设定为准
操作步骤:
① 创建父级Maven工程,打包方式为pom
② 在子工程中声明父工程的应用
③ 在父工程中对依赖的版本进行管理
④ 在子工程中使用父工程中管理的版本不需要书写版本信息
代码示例:
① 创建父级Maven工程,打包方式为pom
<groupId>cn.bruce</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<name>maven-demo</name>
<packaging>pom</packaging>
② 在子工程中声明父工程的应用
使用 <parent></parent> 标签指明子模块的父级模块
<parent>
<artifactId>maven-demo</artifactId>
<groupId>cn.bruce</groupId>
<version>1.0-SNAPSHOT</version>
<relativePath>../maven-demo/pom.xml</relativePath>
</parent>
③ 在父工程中对依赖的版本进行管理
使用 <dependencies></dependencies> 标签进行依赖管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
④ 在子工程中使用父工程中管理的版本不需要书写版本信息
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
2.8. 聚合
项目会伴随着业务的发展变得越来越大,涉及到的模块也会变得更多,如果我们对每个模块都执行声明周期会花费很多时间和精力。Maven提供了聚合的功能,方便我们进行统一的模块管理。
聚合的目的:在一个“总的聚合工程”中管理各个参与的模块
在聚合模块中,使用标签:<module></module> 进行模块管理,配置模块聚合后,在聚合模块执行生命周期操作,聚合模块中的子模会执行和聚合模块相同的生命周期操作,Maven会自动识别子模块间的执行顺序。
具体操作:
① 聚合模块的配置
使用 <modules></modules> 标签配置需要聚合的模块
<modules>
<module>../maven-model-1</module>
<module>../maven-model-2</module>
</modules>
聚合模块中配置好子模块后,使用IDEA中的Maven工具可以查看到,聚合模块中有【root】的标识
在聚合模块执行生命周期后可以看到Maven执行的结果
......
[INFO] maven-parent ....................................... SUCCESS [ 1.361 s]
[INFO] maven-model-1 ...................................... SUCCESS [ 0.003 s]
[INFO] maven-model-2 ...................................... SUCCESS [ 0.002 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.710 s
[INFO] Finished at: 2020-12-14T15:22:16+08:00
[INFO] ------------------------------------------------------------------------
在实际的项目搭建中继承、聚合和依赖传递通常一同使用,来进行项目依赖和生命周期的统一管理。
- 依赖统一管理:在父级Maven工程中配置各个子模块中公共依赖的相关版本信息,子模块继承父级模块方便进行依赖的统一管理;
- 生命周期统一管理:将子模块在父级模块中配置完成后,即可在父级模块上进行统一的生命周期操作;