文章目录
- 1.前言
- 2.pom中的依赖配置
- 2.1.依赖的概念
- 2.2.依赖传递
- 2.3.可选依赖 [optional]
- 2.4.依赖范围 [scope]
- 2.4.1.scope的分类
- 2.4.2.依赖范围对依赖传递的影响
- 2.5.依赖冲突
- 2.5.1.直接依赖
- 2.5.2.间接依赖
- 2.6.依赖排除 [exclusions]
- 3.总结
1.前言
本系列文章记录了 Maven 从0开始到实战的过程,Maven 系列历史文章清单:
(一)5分钟做完 Maven 的安装与配置
(二)使用 Maven 创建并运行项目、聊聊 POM 中的坐标与版本号的规则
(三)Maven仓库概念及私服安装与使用 附:Nexus安装包下载地址
有了前面3篇的基础,我们已经清楚了Maven的基础使用方式,在第二篇文章中,简要的介绍了Maven的坐标与版本号规则,那么本篇呢,会在坐标的基础上进行拓展,讲解其他的标签元素,主要内容包括:
- 依赖传递特性以及如何禁止构件的依赖传递
- 依赖范围的概念以及选择
- 在引入相同构件上的不同版本造成依赖冲突
- 产生依赖冲突时的依赖调解机制
- 如何手动排除依赖
本篇内容基于Maven的3.5版本
2.pom中的依赖配置
我们先看一下一个完整的依赖配置应该是什么样子的:
<dependencies>
<dependency>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
<type></type>
<scope></scope>
<optional></optional>
<exclusions>
<exclusion>
<groupId></groupId>
<artifactId></artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
看起来还是有点复杂的,不过没关系,在下面的内容中会一一解释并使用这些标签。
2.1.依赖的概念
在展开本篇的内容之前,先解释一下什么是依赖,依赖在pom.xml
中可以通过dependency
引入,假设现在有 A、B、C 三个构件,如下图来表示:
我们一般会将这图上的关系描述为,A 依赖 B,A 依赖 C,或者B 是 A 的依赖,C 是 A 的依赖,在这基础上,加上自定义的groupId
,于是在A构件的pom.xml
文件中,就可以写成这样:
<dependencies>
<dependency>
<groupId>com.ls.mavendemo</groupId>
<artifactId>B</artifactId>
<version>1.0.0</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>com.ls.mavendemo</groupId>
<artifactId>C</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
我们可以看到,对B的依赖中写了一个 <type>jar</type>
,而 C 的没有写,type
指的的当前引入的构件类型,我们在实际开发中往往会忽略这个标签,不写的话,默认就是 jar,这里引入的 B 和 C 都是 jar 包。
2.2.依赖传递
依赖传递是Maven中的一个功能特性,简单的说就是底层的依赖会向上传递,依赖传递的好处在于,我们在引入一个三方构建的时候,这个三方构建可以自动的将它所依赖的构建引入到项目中来,而不需要我们再手动去搜索和手动导入。
用 A,B,C 表示构件,则依赖传递如下图所示:
此时,A 虽然没有直接依赖 C,但因为依赖传递的特性,A 依然可以使用 C 里面打包的代码(也不是绝对的,依赖传递还会受到依赖范围的影响,详见下面的2.4依赖范围)。
同样的,通过pom.xml
分别描述 A、B 构建的依赖关系:
<!-- A 依赖 B -->
<dependencies>
<dependency>
<groupId>com.ls.mavendemo</groupId>
<artifactId>B</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<!-- B 依赖 C -->
<dependencies>
<dependency>
<groupId>com.ls.mavendemo</groupId>
<artifactId>C</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
2.3.可选依赖 [optional]
在上面的依赖配置基础上,如果我们不想将 C 传递给 A,则可以在 B 引入 C 的时候,将其设置为可选依赖:
也就是说,引入的依赖如果标记为 optional
则不再向上传递,此时 A 不能再直接使用 C中的代码,可以选依赖在 pom.xml
中表示为:
<!-- B 依赖 C -->
<dependencies>
<dependency>
<groupId>com.ls.mavendemo</groupId>
<artifactId>C</artifactId>
<version>1.0.0</version>
<optional>true</optional>
</dependency>
</dependencies>
这里再提一句为什么会需要禁止依赖传递,举个简单的开发例子,我们在一个 WEB 项目的开发中,将 service
层与 dao
分别拆成了两个构件,service
包依赖 dao
包,但是 service
中只需要使用我们写的主代码,而不需要使用例如数据库驱动、JDBC、ORM框架等依赖,我们就可以在 dao
中使用<optional>
禁止这些持久化相关的包向上传递到service
。
2.4.依赖范围 [scope]
Maven提供了3个 classpath
来表示不同的范围,分别是:编译、运行、测试
,依赖范围的含义就是,当前引入的依赖,会被分配到哪一个(或多个)范围中使用。
2.4.1.scope的分类
依赖范围在 pom.xml
中使用 <scope></scope>
来表示,有5种选项:
compile
:默认选项,也是最常用的选项,在编译、运行、测试的classpath中有引入provided
:只在编译、测试引入,运行时不引入,例如:lombok
runtime
:只在运行、测试引入,编译期不引入,例如:mysql-connector-java
test
:只在测试中有效,例如:JUnit
system
:与provided
相同,但是需要手动指定依赖文件路径
不考虑system
(system会破坏可移植性,不推荐使用,我们直接忘掉它)的关系如下图所示:
下面是针对几种不同的scope
的示例:
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.19</version>
<scope>runtime</scope>
</dependency>
<dependencies>
2.4.2.依赖范围对依赖传递的影响
在上面的内容中提到了,如果不想将 C 传递到 A 可以使用 optional
的方式标记,除此之外,scope
的配置也对依赖传递有影响。
官网中对 scope
的描述如下:
可以看到红框中的部分,这里提到了 compile
可以直接向上传递,provided
和 test
不能向上传递,对 runtime
没有描述,这里我们可以创建3个 Maven 项目验证一下,如下:
上面的代码放到了gitee中:《代码地址下载》,下载后先在dependecy-demo
目录下运行 mvn clean install
。
这里创建了3个 Maven 项目,其中 demo-a
依赖 demo-b
,demo-b
依赖 demo-c
,并在插件中心中下载了一个分析 Maven 依赖的插件 Maven-Helper
,然后按照下面的顺序依次进行验证。
- 将 demo-b 中对 demo-c 的依赖
scope
修改为compile
<dependency> <groupId>com.ls.maven</groupId> <artifactId>demo-c</artifactId> <version>1.0.0</version> <scope>compile</scope> </dependency>
- 依次将 demo-a 中对 demo-b 的依赖
scope
修改为compile
,runtime
,provided
,test
<dependency> <groupId>com.ls.maven</groupId> <artifactId>demo-b</artifactId> <version>1.0.0</version> <scope>compile</scope> </dependency>
- 每修改一次,打开 demo-a 的
pom.xml
,找到 Maven-helper 插件进行分析。
- 将 demo-b 中对 demo-c 的依赖
scope
修改为runtime
重复上面的3个步骤,直到将compile
,runtime
,provided
,test
这4个范围都验证完成
下表中的纵向为 demo-a 中的scope,横向为 demo-b 中的 scope,中间的部分为 demo-c 传递到demo-a的scope,经过组合之后,一共得到了16个结果:
scope | compile | runtime | provided | test |
---|---|---|---|---|
compile | compile | runtime | - | - |
runtime | runtime | runtime | - | - |
provided | provided | provided | - | - |
test | test | test | - | - |
我们把 demo-a 依赖 demo-b 称为直接依赖,demo-a 依赖 demo-c 称为间接依赖,来得出的最终结论为:
provided
与test
范围不会向上传递。compile
范围在向上传递的时候,间接依赖于直接依赖的范围一致。runtime
范围与compile
类似,区别在于直接依赖为compile
时,间接依赖的范围依然是runtime
2.5.依赖冲突
不管是直接依赖,还是间接依赖,只要引入不同verison
的相同构件,就会出现依赖冲突,针对依赖冲突,Maven自带了依赖调解机制,下面用两个例子来说明依赖调解。
2.5.1.直接依赖
先看直接依赖的例子,当A的 pom.xml
中引入了 B 的两个版本,此时哪个版本会生效呢?
<dependency>
<groupId>com.ls.maven</groupId>
<artifactId>demo-b</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.ls.maven</groupId>
<artifactId>demo-b</artifactId>
<version>1.0.1</version>
</dependency>
查看依赖分析后发现1.0.1
生效了,也就是说,后声明的依赖会覆盖先声明的依赖。
2.5.2.间接依赖
看下面这张图,依赖的路径上存在间接依赖,这种情况生效的是哪个版本呢?
猜也能猜的到,生效的肯定是1.0.1
,下面就来验证一下我们的猜测:
- 在A中引入B、和C的1.0.1版本
<dependency> <groupId>com.ls.maven</groupId> <artifactId>demo-b</artifactId> <version>1.0.1</version> </dependency> <dependency> <groupId>com.ls.maven</groupId> <artifactId>demo-c</artifactId> <version>1.0.1</version> </dependency>
- 在B中引入C的1.0.0版本
<dependency> <groupId>com.ls.maven</groupId> <artifactId>demo-c</artifactId> <version>1.0.0</version> </dependency>
回到A的pom.xml
中看一下依赖分析:
可以看到红色的字体,意思是:
从B中间接依赖的C的
1.0.0
版本,由于与1.0.1
版本冲突,所以被省略掉了
也就是说,依赖路径最短的那个依赖会生效,也就是就近原则。
有了这个基础之后,我们再看一个更加复杂的路径,你能判断出D在中的生效的是哪个版本吗?
2.6.依赖排除 [exclusions]
在开发的过程中,有时候我们需要手动的排除一部分传递性的依赖,然后再定义我们需要的依赖,举个简单的例子:
我们在 spring-boot 的项目中使用 redis,首先需要引入一个 starter,这个 starter 中又依赖了 lettuce
这个客户端:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
此时,我们不想使用这个客户端,想切换成 jedis
,那么我们就可以使用exclusions
将 lettuce
排除掉。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.5.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
然后再引入 jedis
的依赖就可以了。
注:这里的exclusion
中不需要使用版本号。
3.总结
在本篇中,我们了解了依赖的相关功能特性:
- 依赖范围:不同的
scope
对应不同的classpathcompile
:默认选项,也是最常用的选项,在编译、运行、测试的classpath中有引入provided
:只在编译、测试引入,运行时不引入,例如:lombok
runtime
:只在运行、测试引入,编译期不引入,例如:mysql-connector-java
test
:只在测试中有效,例如:JUnit
- 依赖传递:在特定的依赖范围下,依赖会向上传递
provided
与test
范围不会向上传递compile
范围在向上传递的时候,间接依赖于直接依赖的范围一致runtime
范围与compile
类似,区别在于直接依赖为compile
时,间接依赖的范围依然是runtime
- 可选依赖:使用
optional
可以禁止依赖向上传递 - 依赖冲突:引入不同版本的相同构件会发生依赖冲突,Maven自带依赖调解机制
- 就近原则:依赖最近中,距离当前项目路径最短的版本会被引入
- 覆盖原则:依赖路径相同时,后定义的依赖版本会覆盖先定义的依赖版本
- 依赖排除:可以手动使用
exclusions
排除依赖构件中的间接依赖
依赖相关的内容就结束了,下一篇会讲解 Maven 模块的聚合与继承,并提供一个 Maven 多模块构件的最佳实践。
如果觉得本文有所帮助的话,可以帮忙点点赞哦~!