Maven

Maven 是什么

Maven 是一个项目管理工具,它包含了一个项目对象模型(Project Object Model),反映在配置中,就是一个 pom.xml 文件。它是一组标准集合,一个项目的生命周期、一个依赖管理系统,另外还包括定义在项目生命周期阶段的插件(plugin)以及目标(goal)。

当我们使用 Maven 的使用,通过一个自定义的项目对象模型pom.xml 来详细描述我们的项目。

Maven 中的有两大核心:

依赖管理:对 jar 的统一管理(Maven 提供了一个 Maven 的中央仓库mvnrepository.com/ ,当我们添加完对应的依赖Maven就会自动去中央仓库下载相关的依赖,并且解决依赖的依赖问题)。

项目构建:对项目进行编译、测试、打包、部署、上传到私服等。

为什么使用 Maven

由于 Java 的生态非常丰富,无论你想实现什么功能,都能找到对应的工具类,这些工具类都是以 jar 包的形式出现的,而jar 包之间会有关联,在使用一个依赖之前,还需要确定这个依赖所依赖的其他依赖,所以,当项目比较大的时候,依赖管理会变得非常麻烦臃肿,这是 Maven 解决的第一个问题。

Maven 还可以处理多模块项目。简单的项目,单模块分包处理即可,如果项目比较复杂,要做成多模块项目,例如一个电商项目有订单模块、会员模块、商品模块、支付模块…,一般来说,多模块项目,每一个模块无法独立运行,要多个模块合在一起,项目才可以运行,这个时候,借助 Maven 工具,可以实现项目的一键打包。

Maven仓库

仓库类型 描述
本地仓库 就是电脑上的本地目录,每台电脑上都一个本地仓库,默认地址:当前用户名\.m2\repository
私服仓库 一般来说都是公司内部搭建的Maven仓库(也称为 二方库),处于局域网中,访问速度快,
这个仓库中存放的jar包一般都是公司内部自己开发或者二次开发封装的jar包
中央仓库 由Apache团建维护,包含了市面上绝大部分的jar包

三个仓库的查找顺序:

image-20231109140408359

实际上,没有特殊需求的话,Maven安装好之后直接就可以用了。一般来说,还是需要稍微配置一下,比如:中央仓库的问题。默认使用 Maven 自己的中央仓库,使用起来网速比较慢,这个时候,可以通过修改配置文件,将仓库改成国内的镜像仓库,国内仓库使用较多的是阿里巴巴的仓库。

1
2
3
4
5
6
7
<!-- 阿里云仓库 -->
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>

本地仓库默认位置在 当前用户名\.m2\repository,这个位置可以自定义,但是不建议大家自定义这个地址,有几个原因:

  1. 虽然所有的本地的 jar 都放在这个仓库中,但是并不会占用很大的空间。
  2. 默认的位置比较隐蔽,不容易碰到

技术上来说,当然是可以自定义本地仓库位置的,在 conf/settings.xml 中自定义本地仓库位置:

1
<localRepository>xxx</localRepository>

在 cmd 中敲并回车执行:mvn help:system

首次执行 mvn help:system 命令,Maven会自动帮我们到Maven中央仓库下载缺省的或者Maven中央仓库更新的各种配置文件和类库(jar包)到Maven本地仓库中。

Maven的核心概念

GAV坐标

在平面几何中坐标(x,y)可以标识平面中唯一的一点。在maven中坐标就是为了定位一个唯一确定的jar包。Maven世界拥有大量构建,我们需要找一个用来唯一标识一个构建的统一规范,拥有了统一规范,就可以把查找工作交给机器。

Maven坐标主要组成(GAV) :

【G】groupId:定义当前Maven组织名称;

【A】artifactId:定义实际项目名称;

【V】version:定义当前项目的版本;

GAV坐标的作用:确定一个jar包在互联网上的位置。

依赖管理

scope 依赖范围

image-20231109144335879

其中依赖范围 scope 标签用于控制依赖的范围,即指定依赖的有效范围,以便在不同的环境下进行不同的构建或部署:

  • compile默认是编译依赖范围。对于编译,测试,运行三种classpath都有效;
  • test:测试依赖范围。只对于测试classpath有效;
  • provided:已提供依赖范围。对于编译,测试的classpath都有效,但对于运行无效。
  • runtime:运行时范围。项目打包运行时才有效,例如:jdbc驱动;
  • system:类似 provided,但需要显式地指定依赖的路径或文件。
  • import:该 scope 只用于在 pom.xml 中使用 dependencyManagement 元素来管理依赖版本号,不会实际被引入到项目中。

Demo | provided用法

1
2
3
4
5
6
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
<scope>provided</scope>
</dependency>

通常情况下,lombok 依赖的 scope 被定义为 provided。这是因为 lombok 库在编译时需要在 classpath 中存在,但在运行时不需要被打包到部署包中,因为它只是一个编译时工具库(在编译的完成之后,该工具包的作用就已经结束了)。通过编译lombok注解的得到的Class类可以看出来,编译之后对应的lombok的注解都已经被同化为JDK的原生Java代码了,也就是说编译完成之后,使用lombok的注解就可以直接运行了,而不再依赖lombok。因此,将其设置为 provided 可以避免将不必要的 jar 包打包到部署包中,从而减小部署包的大小。同时,在使用 lombok 时,需要将其添加到 IDE 或编译器的插件中,以确保在编译时正常使用 lombok 注解。

Demo | import + <dependencyManagement>用法

Maven 的 dependencyManagement 元素用于管理项目的依赖版本号,它可以集中管理项目的依赖版本号,避免在多个模块中重复声明版本号,同时方便版本的统一升级。如某版本的spring-boot-starter-parent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.12</version>
</dependency>
</dependencies>
</dependencyManagement>

自己的项目在使用spring-boot-starter-parent作为parent,同时在需要使用这些依赖的子模块中,不需要再指定依赖的版本号,只需要声明对应的 groupId 和 artifactId。例如:

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>

这样,子模块会自动使用父 POM 中声明的版本号来获取依赖。如果需要升级依赖的版本,只需要在父 POM 中修改对应的版本号即可,所有子模块都会自动继承这个版本号。

另外,如果某个子项目需要特定另外的一个版本,只需要添加version标签元素进行声明即可, 会自动覆盖版本 - 就近原则

注意,dependencyManagement 只是用于管理依赖版本号,它并不会引入实际的依赖。如果需要在子模块中引入依赖,还需要在 dependencies 元素中声明具体的依赖信息。

如果子模块作用域是import:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<scope>import</scope>
</dependency>
</dependencies>

由于 scope 被设置为 import,这些依赖也不会被实际引入到子模块中,而是只用于管理版本号。

依赖传递

如果想进行依赖传递,最好将所有的依赖的范围都设定为默认的compile级别,依赖传递分为两种:

  • 直接依赖
  • 间接依赖

例如:test2 依赖 test1,test3依赖test2,则test2 直接依赖 test1,test3间接依赖test1;依赖关系图如下:

image-20231109150716918

当第二直接依赖的范围是compile的时候,依赖可以传递;当第二直接依赖的范围是test的时候,依赖不会传递。

依赖冲突

假如test1使用junit4.10依赖,并且scope是compile,那test2、test3都可以使用test1的junit4.10,因为依赖传递下来了;

假如test2又引入了使用junit4.9依赖,那test3会使用junit4.9【就近原则的一个依赖】;

可选依赖

<optional> 属性表示依赖是否可选;也可以理解为是否向下传递。

1
<optional> true/false<optional> 

在依赖中添加optional选项决定此依赖是否向下传递:

  • true:不传递;
  • false:传递,默认为false;

排除依赖

用maven管理库依赖,有个好处就是连同库的自身依赖(间接依赖)jar包也全部都一起下载,也就是依赖传递的功能。这种特性免去手工添加的麻烦,但同时也带来了同一个jar包会被下载了不同版本的问题,即jar包的版本冲突。好在pom的配置里面允许用<exclusion>来排除一些不需要同时下载的依赖jar 。

引入dubbo 依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibabaxm</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.3</version>
</dependency>

但是该包依赖org.springframework 2.5.6.SEC03的jar包, 但是项目本身又引入了springframework 4.3.3的jar包,所以这种依赖反倒成了工程瘦身的负担,并可能会出现jar包冲突的情况。

怎么解决这个问题呢?

答案: 使用 exclusions,排除多余的依赖.

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.3</version>
<exclusions>
<exclusion>
<artifactId>spring</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>

这样,就将引入的 dubbo 2.5.3 中依赖的springframework的依赖排除了。

如果想要去掉全部的依赖,可以使用通配符 * :

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.3</version>
<exclusions>
<exclusion>
<artifactId>*</artifactId>
<groupId>*</groupId>
</exclusion>
</exclusions>
</dependency>

这样就将dubbo 2.5.3 中所有依赖的jar包都排除了,就是单纯地引入dubbo 2.5.3这一个依赖。

Maven生命周期命令

Maven项目的目录结构

使用Maven创建Web项目,IDE会自动生成一下目录结构:

1
2
3
4
5
6
7
8
9
10
ProjectName
|-src
| |-main
| | |-java —— 存放项目的 .java 文件
| | |-resources —— 存放项目资源文件,如 spring, Mybatis等的配置文件
| |-test
| |-java —— 存放所有测试.java文件,如JUnit测试类
| |-resources —— 测试资源文件
|-target —— 目标文件输出位置,例如 .class、.jar、.war文件
|-pom.xml —— maven项目核心配置文件

Maven生命周期

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

生命周期可以理解为构建工程的步骤。

在Maven中有三套相互独立的生命周期,请注意这里说的是“三套”,而且“相互独立”,这三套生命周期分别是:

  • Clean Lifecycle: 在进行真正的构建之前进行一些清理工作;
  • Default Lifecycle: 构建的核心部分,编译,测试,打包,部署等等;
  • Site Lifecycle: 生成项目报告,站点,发布站点;

Clean 生命周期:清理项目

Clean生命周期一共包含了三个阶段:

  1. pre-clean:执行一些需要在clean之前完成的工作;
  2. clean: 移除上一次构建生成的所有文件;
  3. post-clean:执行一些需要在clean之后立刻完成的工作;

也就是说,mvn clean 等同于 mvn pre-clean clean 两种操作;如果执行 mvn post-clean ,那么 pre-cleanclean 都会被运行,这是Maven很重要的一个规则,可以大大简化命令行的输入。

Default 生命周期:构造项目

Default生命周期是Maven生命周期中最重要的一个,绝大部分工作都发生在这个生命周期中。这里只解释一些比较重要和常用的阶段:

validate

  • 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 接受编译好的代码,打包成可发布的格式,如 JAR 。

  • pre-integration-test
  • integration-test
  • post-integration-test
  • verify

install 将包安装至本地仓库,以让其它项目依赖。

deploy 将最终的包复制并传输到远程仓库,以让其它开发人员与项目共享

运行任何一个阶段的时候,它前面的所有阶段都会被运行,这也就是为什么我们 运行mvn install 的时候,代码会被编译,测试,打包,安装到本地仓库的原因。此外,Maven的插件机制是完全依赖于Maven的生命周期的,因此理解生命周期至关重要。

Site 生命周期:生成项目站点

这里经常用到的是site阶段和site-deploy阶段,用以生成和发布Maven站点,这可是Maven相当强大的功能,Manager比较喜欢,文档及统计数据自动生成,很方便,很好看。

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

Maven生命周期命令

使用IDEA创建Maven项目,然后进入项目目录,测试Maven各个命令的作用:

编译:mvn compile

1
mvn compile

命令执行完毕后,会生成target目录,该目录中存放了项目编译后的字节码文件。

清除:mvn clean

1
mvn clean

命令执行完毕后,会将compile生成的target目录删除。

测试:mvn test

1
mvn test

命令执行完毕后,会在target目录中生成三个文件夹:surefire、surefire-reports(测试报告)、test-classes(测试的字节码文件)。

打包:mvn package

1
mvn package

命令执行完毕后,会在target目录中生成一个项目文件,为:jar包、war包【web项目】。

安装:mvn install

1
mvn install

目的就是将打包好的jar包上传到本地仓库中,执行完毕后,会在本地仓库中出现安装后的jar包,本地的其他工程就可以直接引用了,在使用Maven进行子模块开发的时候最能体现。

部署: mvn deploy

1
mvn deploy

目的就是将打包好的jar包部署到指定的远程仓库,比如:团体或者公司的仓库。

Maven也可以使用组合命令,如:

  • mvn clean compile
  • mvn clean test
  • mvn clean package
  • mvn clean install

Maven的常用命令

Maven设置版本号命令

对于多module项目,可以使用versions-maven-pluginmvn versions:set命令升级版本号;统一修改pom的版本号,及子模块依赖的版本号:

1
mvn versions:set -DnewVersion=xxx

如果有问题,可以回退版本号:

1
mvn versions:revert

如果没问题,然后执行如下命令,确认提交版本号:

1
mvn versions:commit

Maven依赖树命令

查看项目的依赖模型,可以使用Maven依赖树:

1
mvn dependency:tree

Maven的常用标签

Maven的<relativePath/>标签

搭建Maven项目时,子模块指定父模块,经常在<parent>标签中添加<relativePath/>标签,如下:

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

这个<relativePath/>标签的意思就是parent的路径,具体来说就是从什么地方引用这个parent项目,即这个parent项目的pom在哪里?

  • 默认没有<relativePath/>标签,那就从默认的路径:../pom.xml,会从本地路径中获取parent的pom文件,建立多个模块时就是在这个情况。
  • <relativePath/>,指定了relativePath,但是值是空的,那就始终从仓库中获取,不从本地路径获取。场景的就是使用springboot构建项目。
  • <relativePath>xx<relativePath/>,指定了一个路径去获取parent的pom文件。

从父级仓库查找依赖版本,MAVEN构建jar包时查找顺序:relativePath元素中的地址 > 本地仓库 > 远程仓库


参考文档:

maven scope标签的作用

学Maven,这篇万余字的教程,真的够用了!