目录
- 一、依赖传递
- 二、依赖范围
- 三、依赖范围对传递依赖的影响
- 四、依赖调节
- 五、可选依赖
- 六、排除依赖
- 七、依赖归类
- 八、依赖管理
一、依赖传递
Maven 依赖传递是 Maven 的核心机制之一,它能够一定程度上简化 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 在对项目进行编译、测试和运行时,会分别使用三套不同的 classpath。Maven 项目构建时,在不同阶段引入到 classpath 中的依赖时不同的。
我们可以在 POM 的依赖声明使用 scope 元素来控制依赖与三种 classpath(编译 classpath、测试 classpath、运行 classpath )之间的关系,这就是依赖范围。
下面我们首先要对三种classpath进行了解:
- 我们使用idea跑项目当中src/main/java目录下的代码实际上就是运行的当前项目target/classes目录下的字节码文件。
- 我们使用idea跑项目当中src/test/java目录下的代码实际上就是运行的当前项目target/test-classes目录下的字节码文件。
- 为什么这里要说是war包而不是jar包,因为jar包是可以直接运行的,而war包是需要依赖于tomcat的。
可选配置有 compile、test、provided、runtime、system、import,若不指定则默认 compile。 首先我们要知道classes和test-classes目录是不存jar包的,项目所依赖的jar包直接使用的是本地maven仓库的,依赖范围更为通俗的理解,其实就是给依赖包打标记,例如将 A 依赖包标记为“compile”,Maven 就知道 A 依赖包不仅运行classes目录的代码要用,运行test-classes也要用,打成的war包也要用(打包就是需要把依赖的包也打包到war,这样war就可以不依赖于本地仓库而放到tomcat当中进行运行了)。
- compile(编译依赖范围):使用此依赖范围的Maven 依赖,
对于编译、测试、运行三种classpath 都有效
。典型的例子是spring-core,在编译、测试和运行的时候都需要使用该依赖。 - test(测试依赖范围):
只对于测试classpath有效
,在编译主代码或者运行项目的使用时将无法使用此类依赖。典型的例子是junit,它只有在编译测试代码及运行测试的时候才需要。 - provided:
对于编译和测试classpath有效
,但在运行时无效。典型的例子是 servlet-api,,编译和测试项目的时候需要该依赖,假如需要打包成war然后放到tomcat当中运行,由于tomcat已经提供,就不需要Maven重复地引入一遍。所以将 scope 设置为 provided 的依赖不会参与项目的war打包
。假如打包为jar,设置与不设置provided并不会影响maven将依赖打包到jar当中
。
说到provided,这里就要说到
<dependency>
下的子标签<optional>
,这两者的区别在于:
1、<optional>
为true 表示依赖不会传递。例如:x依赖B,B又依赖于A(x->B->A),则A中设置<optional>
为true的依赖不会被传递到x中。假如当前项目某个依赖<optional>
为true 并不会影响该依赖在当前项目的打包,他只会影响所依赖当前项目的其他项目打包。
2、<scope>
为provided 代表的是该依赖不参与打war包。
在目标容器中已经提供了这个依赖,无需在提供
关于optional的详解:https://blog.csdn.net/weixin_43888891/article/details/130510971
- runtime(运行时依赖范围): 运行时依赖范围:对于测试和运行class-path有效,但在编译主代码时无效(对编译的classpath无效)。典型的例子是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。
- system(系统依赖范围):需要引用第三方本地jar包的时候使用,案例:https://blog.csdn.net/weixin_43888891/article/details/130611728
- import(导入依赖范围): 导入依赖范围,该依赖范围只能与 dependencyManagement 元素配合使用,其功能是将目标 pom.xml 文件中 dependencyManagement 的配置导入合并到当前 pom.xml 的 dependencyManagement 中。案例:https://blog.csdn.net/weixin_43888891/article/details/130520345
依赖范围与三种 classpath 的关系一览表,如下所示。
三、依赖范围对传递依赖的影响
项目 A 依赖于项目 B,B 又依赖于项目 C,此时我们可以将 A 对于 B 的依赖称之为第一直接依赖,B 对于 C 的依赖称之为第二直接依赖。
B 是 A 的直接依赖,C 是 A 的间接依赖,根据 Maven 的依赖传递机制,间接依赖 C 会以传递性依赖的形式引入到 A 中,但这种引入并不是无条件的,它会受到依赖范围的影响。
传递性依赖的依赖范围受第一直接依赖和第二直接依赖的范围影响,如下表所示。
注:上表中,左边第一列表示第一直接依赖的依赖范围,上边第一行表示第二直接依赖的依赖范围。交叉部分的单元格的取值为传递性依赖的依赖范围,若交叉单元格取值为“-”,则表示该传递性依赖不能被传递。
通过上表,可以总结出以下规律:
- 当第二直接依赖的范围是 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 依赖调节的第一原则已经无法解决,需要使用第二原则:先声明者优先。
A 的 POM 文件中配置如下。
<dependencies>
...
<dependency>
...
<artifactId>B</artifactId>
...
</dependency>
...
<dependency>
...
<artifactId>X</artifactId>
...
</dependency>
...
</dependencies>
有以上配置可以看出,由于 B 的依赖声明比 X 靠前,所以间接依赖 D(1.0)将从 A->B->D(1.0) 路径引入到 A 中。
五、可选依赖
可选依赖就是指的optional标签,关于optional的详解:https://blog.csdn.net/weixin_43888891/article/details/130510971
六、排除依赖
传递性依赖可以帮助我们简化项目依赖的管理,但是同时也会带来其他的不必要的风险,例如:会隐式地引入一些依赖,这些依赖可能并不是我们希望引入的,或者这些隐式引入的依赖是 SNAPSHOT 版本的依赖。依赖的不稳定导致了我们项目的不稳定。
在我们的项目中,spring-boot-starter-test 依赖中排除了 junit-vintage-engine 依赖是由于我们使用的 springboot 版本是 2.2.6-RELEASE,对应的 Junit 版本是 5.x,但 junit-vintage-engine 依赖中包含了 4.x 版本的 Junit,此时我们就可以将该依赖排除。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
关于 exclusions 元素及排除依赖说明如下:
- 排除依赖是控制当前项目是否使用其直接依赖传递下来的接间依赖;
- exclusions 元素下可以包含若干个 exclusion 子元素,用于排除若干个间接依赖;
- exclusion 元素用来设置具体排除的间接依赖,该元素包含两个子元素:groupId 和 artifactId,用来确定需要排除的间接依赖的坐标信息;
- exclusion 元素中只需要设置 groupId 和 artifactId 就可以确定需要排除的依赖,无需指定版本 version。
七、依赖归类
在我们实际的开发过程中,我们可能会需要整合很多第三方框架,在整合这些框架的时候,往往需要在 pom.xml 里面添加多个依赖来完成整合。而这些依赖往往是需要保持相同版本的,在升级框架的时候,都是要统一升级到一个相同的版本。
如下图,我们可以看到,在引入 dubbo 框架的时候,我们需要引入两个相关的依赖,而且版本号是相同的,这个时候,我们就可以把对应的版本号提取出来,放到 properties 标签里面,作为一个全局参数来使用。类似于 Java 语言中抽象的思想。
这时候,我们可以看到,如果在将来的某一天我们需要升级升级 dubbo 框架对应的版本,只需要修改 properties 中的版本号,就能将所有依赖的版本一起升级。
八、依赖管理
依赖管理就是指的dependencyManagement标签,直接看这一篇文章即可:https://blog.csdn.net/weixin_43888891/article/details/130520345