Maven(一): 依赖管理

Long Long Ago,Java开发的依赖问题一直都是需要群众手工进行管理。耗时耗力不说,还TM容易出幺蛾子。直到后来Maven的出现,才彻底地扭转了这一局面。将人民群众从剪不断理还乱的依赖关系中解脱出来,全心全意地投入到为PM的服务当中去。相信很多人对Maven如何管理依赖已经有一个基础的认知了,此处就不再多言了。这里将主要对Maven依赖管理中不常见的容易忽略的知识点进行介绍

abstract.png

依赖范围 Scope

Maven在编译、测试、运行(含打包)阶段中所需的依赖并不完全一致,所以Maven通过三种不同的classpath实现在不同阶段引入所需的依赖:编译classpath、测试classpath、运行classpath

依赖范围:控制依赖与三种classpath的关系。可在POM文件的依赖元素dependency中的scope元素中进行配置,缺省值 compile

常见的依赖范围有:

  • compile : 编译依赖范围,scope元素的缺省值。使用此依赖范围的Maven依赖,对于三种classpath均有效,即该Maven依赖在上述三种classpath均会被引入。例如,Spring框架的核心依赖在编译、测试、运行过程都是必须的
  • 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 :导入依赖范围。其不会对上述三种classpath造成任何实际影响

依赖范围与三种classpath的关系一览表,如下所示:

Scope 编译classpath 测试classpath 运行classpath e.g.
compile Y Y Y spring framework core
test - Y - Junit
provided Y Y - servlet-api
runtime - Y Y JDBC驱动实现
system Y Y - 非Maven仓库的本地依赖

Note:

  1. 在打包阶段,使用的是运行classpath。即引入到运行classpath中的Maven依赖会被一起打包

传递性依赖与依赖范围

figure 1.png

传递性依赖机制

项目A中,我们为了实现某一个功能通常会引入第三方库,这里是一个compile依赖范围的B依赖。而B依赖同时又依赖于另一个compile依赖范围的C组件。那么对A而言,C就是它的一个传递性依赖。在Maven中,其会将我们在POM文件中显式声明的直接依赖(本例的B依赖)引入到项目中,对于必要的间接依赖(本例的C依赖)则会以传递性依赖的形式自动地引入到项目A中,而无需我们手动显式地在POM文件中声明C依赖来引入。Maven的传递性依赖机制,大大地减少了人工维护间接依赖的复杂度

传递性依赖的依赖范围

项目A依赖于B组件,B组件依赖于C组件。则我们将A对于B的依赖称之为第一直接依赖,B对于C的依赖称之为第二直接依赖。根据上文可知,A对于C的依赖是传递性依赖,必要的间接依赖C将通过传递性依赖机制,被自动引入到A中。那么如何判定一个间接依赖是否有必要被引入呢?间接依赖被引入后其依赖范围又是什么呢?答案其实很简单,就是通过第一直接依赖的依赖范围和第二直接依赖的依赖范围之间的关系,来判定是否有必要引入间接依赖以及确定引入间接依赖后其依赖范围,如下表所示。若结果为N,则意味着该传递性依赖为非必要的,无需引入;否则,该间接依赖为必要的并自动引入该间接依赖,且引入后该传递依赖的依赖范围如下表单元格中的文字所示

figure 2.jpeg

传递依赖的依赖范围规律总结如下:

  • 当第二直接依赖的依赖范围为compile : 传递依赖的依赖范围同第一直接依赖的依赖范围一样
  • 当第二直接依赖的依赖范围为test : 传递依赖将不会被引入
  • 当第二直接依赖的依赖范围为provided : 只有当第一直接依赖的依赖范围亦为provided时,传递依赖才会被引入,且依赖范围依然是provided
  • 当第二直接依赖的依赖范围为runtime : 除了第一直接依赖的依赖范围为compile时传递依赖的依赖范围为runtime外,其余情况下,传递依赖的依赖范围同第一直接依赖的依赖范围一样

依赖调解

在Maven中由于传递性依赖的机制,一般情况下我们不需要关心间接依赖的管理。而当间接依赖出问题时,我们需要知道该间接依赖是通过哪条依赖路径引入的。特别是该间接依赖存在多条引入路径时,确定间接依赖引入的路径就显得尤为重要。当一个间接依赖存在多条引入路径时,为避免依赖重复Maven会通过依赖调解来确定该间接依赖的引入路径

依赖调解遵循以下原则,优先使用第一原则,当第一原则无法解决时,则通过第二原则解决

  • 第一原则: 路径最短者优先
  • 第二原则: 第一声明者优先

假设在项目A中存在如下依赖关系:

1
2
A -> X -> Y -> Z(2.0)   // dist(A->Z) = 3
A -> M -> Z(2.1) // dist(A->Z) = 2

项目A依赖的Z组件有2个版本,很显然不可能同时引入两个版本的间接依赖。这里可以看到,Z(2.0)依赖的依赖路径长度为3,Z(2.1)依赖的依赖路径长度为2。根据依赖调解的第一原则——路径最短者优先。所以,2.1版本的Z组件将通过 A -> M -> Z(2.1) 路径被引入到A中

假设在项目B中存在如下依赖关系,间接依赖W在两条依赖路径中的路径长度均为2,这时候就无法通过依赖调解的第一原则来确定引入路径。此时需要使用依赖调解的第二原则——第一声明者优先。根据项目B的POM文件中直接依赖K、P的声明顺序,先声明的直接依赖,则其间接依赖即通过该路径被引入

1
2
B -> K -> W(1.0)        // dist(B->W) = 2
B -> P -> W(2.0) // dist(B->W) = 2

项目B的POM文件内容如下所示,由于P依赖比K依赖先声明,则2.0版本的的W组件将通过 B -> P -> W(2.0) 路径被引入到B中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
...
<dependency>
...
<artifactId>P</artifactId>
...
</dependency>
...
<dependency>
...
<artifactId>K</artifactId>
...
</dependency>
...
</dependencies>

可选依赖 option 与 排除依赖 exclusions

可选依赖 option

可选依赖是通过项目中的POM文件的依赖元素dependency下的option元素中进行配置,只有显式地配置项目中某依赖的option元素为true时,该依赖才是可选依赖;不设置该元素或值为false时,该依赖即不是可选依赖。其意义在于,当某个间接依赖是可选依赖时,无论依赖范围是什么,其都不会因为传递性依赖机制而被引入

假设在项目A中存在如下依赖关系

1
2
3
A -> M
M -> X(可选依赖)
M -> Y(可选依赖)

当上述依赖的依赖范围均为compile,则间接依赖X、Y将通过传递性依赖机制被引入到A中。但是由于M中对X、Y的依赖均是可选依赖。故X、Y依赖都不会被传递到项目A中,即X、Y依赖不会对项目A产生任何影响

figure 3.jpeg

这里简单介绍下option元素的实际应用场景,我们在开发项目A的过程中,需要依赖一个第三方持久层的M组件。而这个M组件对两种不同数据库均支持(e.g., MySql、Oracle),故M组件中需要依赖这两种不同数据库的驱动实现——X、Y组件。对于开发项目A的工程师而言,他可能在项目开发只需要使用其中一种数据库(例如MySql,其驱动实现为X依赖)。如果数据库驱动实现X、Y不是可选依赖,则均会传递到项目A中。虽然一般情况下这不会引发任何问题,但是会因引入不必要的依赖Y而造成项目体积增大,如果该M组件还支持更多类型的数据库,就会引入更多的不必要的数据库驱动实现依赖进来;而如果M组件中的X、Y依赖是可选依赖的话,则工程师就可以根据实际需要在A项目的POM文件中显式地引入所需数据库的驱动实现依赖即可

当然,一般情况下是不推荐使用可选依赖的,使用可选依赖一般是因为项目中支持、实现多种特性所造成的。就如同例子中的持久层组件M,其支持多种类型数据库。根据对象的单一职责思想,这种多特性的设计有时候并不是一个好的设计。实际上可以针对不同类型的数据库分别写持久层组件M1、M2。这样在使用时,就可以直接根据使用的数据类型依赖各自对应的持久层组件即可。而且由于组件M1、M2中的驱动实现依赖不是可选依赖,其可通过传递依赖的形式引入到项目A中,从而无需在A项目的POM文件中显式地引入所使用数据库的驱动实现依赖

figure 4.png

排除依赖 exclusions

间接依赖是可以通过传递性依赖机制引入到当前项目中,而有时候第三方组件B的C依赖由于版本(1.0)过低存在安全漏洞。我们期望能够将该间接依赖直接剔除出去,不通过传递依赖的形式引入到项目中。这时即可通过exclusions元素实现,该元素下可以包含若干个 exclusions 子元素,然后再在POM中显式地引入合适版本(3.3)的C依赖

figure 5.png

值得一提的是,在exclusion元素中,只需给定groupId、artifactId即可确定依赖,而无需指定版本version。POM实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
...
<dependency>
<groupId>com.apple</groupId>
<artifactId>B</artifactId>
<version>2.3</version>
<exclusions>
<exclusion>
<groupId>com.google</groupId>
<artifactId>C</artifactId>
</exclusion>
</exclusions>
</dependency>
...
<dependency>
<groupId>com.google</groupId>
<artifactId>C</artifactId>
<version>3.3</version>
</dependency>
...
</dependencies>

依赖的常用命令

1. 查看引入的依赖列表

通过下述命令,可以查看当前项目中所有引入的依赖、版本、依赖范围信息列表,包含直接依赖和通过传递依赖引入的间接依赖

1
mvn dependency:list

效果如下所示

figure 6.jpeg

2. 查看引入的依赖树

通过下述命令,可以查看当前项目中所有引入的依赖、版本、依赖范围信息。其结果以树形结构图的形式展示,第一级的依赖(红框部分)即为直接依赖,剩下的(绿框部分)则为通过传递依赖引入的间接依赖

1
mvn dependency:tree

效果如下所示

figure 7.jpeg

3. 依赖分析

通过下述命令,可以分析当前项目中依赖的使用情况

1
mvn dependency:analyze

效果如下所示

figure 8.jpeg

该分析报告中有两个部分需要我们重点关注

  • Used undeclared dependencies found : 指项目中使用到的但未显式声明的依赖,即在项目中的代码使用了通过传递引入的依赖。如上图绿框所示
  • Unused declared dependencies found : 指项目中未使用的但显式声明的依赖,如上图红框所示。由于该命令实际上是分析编译阶段中所使用的依赖,而对于测试、运行阶段依赖的使用情况则无法分析,所以,这里列出的未使用的依赖,可能是冗余的依赖,但也可能是测试、运行阶段所必须的依赖。故应谨慎删除

参考文献

  1. Maven实战 许晓斌著
0%