目录
依赖存放的地方:Repository(仓库)
通过pom文件查找依赖的依赖
循环依赖
依赖版本冲突和依赖调解
依赖传递
依赖的作用域
Exclusions
依赖管理的简化:Dependency Management
依赖导入
总结:
依赖管理是项目管理中非常重要的一环。几乎任何项目开发的时候需要都需要使用到库。而这些库很可能又依赖别的库,这样整个项目的依赖形成了一个树状结构,而随着这个依赖的树的延伸和扩大,一系列问题就会随之产生。Maven提供了一套完整的方法,让使用者可以轻松的管理项目的依赖。
复习下前面的知识:
依赖和插件可以统称为构件(Aritificial)
在Maven中存储构件的地方叫仓库(Repository)。
在配置文件中对依赖的仓库使用
仓库大致可以分为两种
本地仓库(Local Repository),本地计算机的仓库,项目声明需要某个构件的时候会先到本地仓库中寻找,会缓存之前在中央仓库或者远程仓库下载的构件。
远程仓库(Remote Repository),远程服务器上的仓库,当在本地仓库找不到构件的时候就会先到远程仓库中把构件下载到本地仓库。远程仓库可以大致分为三类。
(1)中央仓库(Central Repository),大部分构件都会存储在中央仓库中。网上有些博客会把中央仓库独立到远程仓库外,但是按照官方文档,中央仓库应该是属于远程仓库中的一种,不过这个中央仓库比较特别,在不进行任何配置的情况下,Maven回到中央仓库中查找构件。
(2)可以在互联网上访问的第三方库的仓库,比如JBoss的仓库。
(3)内部仓库(internal repository),团体或者公司搭建的用于内部访问的内部仓库,用于存储一些内部使用的构件,以及缓存从中央仓库或者别的仓库下载的构件。
需要注意的是上面对远程仓库的划分是为了便于读者理解,在配置文件中都是同样的配置方式,本质上都是Remote Repository。
做一个比喻就是,把一个项目比作一道菜,那么这个菜的食谱就对应项目的pom文件,食谱上写了菜需要什么食材,就像pom文件声明了项目需要哪些构件。 本地Repository就是家中存放食材的地方,远程Repository就菜市场。如果家里已经有了做菜的食材那么就可以直接开始烹饪。但是如果没有的话就需要去菜市场买到家里(从远程Repository下载到本地Repository)。有一点区别的就是同一个构件可以被多个项目复用,但是一个食材只能做一道菜,另一道菜需要同样的食材还需要到菜市场去买。
如果多个地方配置有多个Repository,那么Maven要下载构件的时候,会按照一定的顺序访问这些Repository,直到在某个Repository中找到需要的构件。
如果有多处配置,按照如下顺序访问这些配置。
1.有效的setting.xml(需要绑定profile配置)
用户配置文件setting.xml
2.有效的POM文件
3.项目依赖路径上的POM文件
注:上文的profile标签主要是用于不同运行环境时切换不同的配置,这里就不详细介绍。
如果一处配置中有多个Repository,那么按照Repository的声明顺序进行查找。
java - How to set order of repositories in Maven settings.xml - Stack Overflow(
这里setting.xml中的repository的读取顺序似乎有个bug。
2.x版本的Maven是按照id的字典序查找的,3.x版本修复这个bug,按照声明顺序进行查找。)
注意在没有任何配置的默认情况下,是加载超级POM文件。而超级POM文件中的声明如下。该POM声明中央仓库,即id为central的repository,所以默认到中央仓库中查找构件的原理就是超级POM中声明了中央仓库。这也说明了中央仓库是一种特殊的远程仓库而非独立于远程仓库外的一种仓库。
maven中每个项目都有一个pom文件,项目打包之后上传至repository的时候这个pom文件也会随之上传。而下载的时候会把pom文件和jar包一起下载下来。如图中jakarta.mail-1.6.4.jar下的jakarta.mail-1.6.4.pom。
maven下载好这个依赖之后,会读取依赖的pom文件,下载pom文件里面声明的依赖。
比如pom文件里面声明了com.sun.activation.jakarta.activation,所以maven会继续下载com.sun.activation.jakarta.activation这个依赖,至于下载的版本则根据别的方式决定,后面会详细讲的。
随着项目的依赖越来越多,会产生一系列问题。典型的两个便是依赖版本冲突、循环依赖。
循环依赖是指项目依赖之间直接或间接的相互依赖。比如a依赖b,b依赖c,c依赖a。
如下图所示
如果项目中存在循环依赖的情况,maven会报错,因为循环依赖的情况出现说明项目结构有不合理的地方,需要开发者自行调整。
依赖版本冲突是指项目依赖路径上对同一个依赖有两个版本。如下图,A依赖B,而B依赖于1.1.0版本的D。同时A依赖C,C又依赖于1.2.0版本的D。但是只能使用一个版本的D,maven提供了一种机制确定使用哪个版本的D,那就是依赖调解(Dependency mediation)
所谓的依赖调解其实很简单,按照下面两个规则确定使用哪个版本的依赖。
1.最近原则,取依赖路径上最近的一个版本。比如下面的情况,因为A到D1.0的路径最近,就取D1.0作为使用的版本。
2.如果有多个版本满足最近原则,则取最先声明的路径上的版本。比如下面A到D1.0和D2.0的距离相同,但是因为D2.0的路径先声明,所以取D2.0。
2.如果有多个版本满足最近原则,则取最先声明的路径上的版本。比如下面A到D1.0和D2.0的距离相同,但是因为D2.0的路径先声明,所以取D2.0。
开发者也可以在pom.xml声明对D的依赖来确定D的版本,这个时候的依赖路径如下,所以这是满足最近原则的。
依赖传递是指项目依赖于依赖的依赖,听起来很绕口。举个例子,项目A依赖于B,B依赖于C,那么正常情况下A依赖于C。maven提供了一些方法让A可以避免对C的依赖,这样可以简化项目的依赖路径。
这些方法总结起来就下面两个:
(1)依赖的作用域
(2)optional和exclusion关键字。
depedency下有一个scope配置项,这个便是用于配置依赖的作用域。
其中scope可取的值有4个,scope(作用域)限制了依赖的传递,是maven管理依赖传递中的一环。只有特定作用域的依赖会参与依赖传递,这样就减少了依赖冲突的情况。
scope有以下几个值可以取
compile(默认)、test、runtime、provided、system、import。
其中import和依赖管理有关,后面介绍。目前主要介绍下compile(默认)、test、runtime、provided、system这5个作用域。
compile在编译、运行、测试的时候都会导入依赖项。
下面介绍下每个作用域的作用和对应的应用场景
注意:
如果在编译时引入了依赖,那么在源代码中引入依赖包下的类不会报编译错误,反之在IDE则会报编译错误。
如果在运行时引入了依赖,那么可以用Class.forName()方法加载该依赖包下的类,反之会抛出
java.lang.ClassNotFoundException异常。
如果在测试的时候引入了依赖,那么在测试代码中引入依赖包下的类不会报编译错误,且可以Class.forName()加载依赖下的类。
compile默认作用域,在编译、运行、测试的时候均会引入该依赖。一般情况下是使用这个作用域。
test只有在测试的时候会导入依赖项,比如juni库。如果一些依赖只有在测试的时候会用到,比如一些测试库,那么需要设置其作用域为test。
runtime在编译时不会导入依赖项,运行的时候会导入依赖项。比如各种数据库驱动的jdbc实现库,编译的时候不会使用到这些库,但是运行的时候jdbc会动态加载会用到这些库。
provided作用域在编译和测试的时候会引入依赖,但是运行时不会。有些库在由运行环境提供,为了避免和运行环境已有库冲突需要设置作用域为provided。比如java web中的servlet库,tomcat容器自带这个库,所以编写web项目的时候需要声明servlet依赖的作用域为provided。
system作用域和runtime类似,区别是由当前系统提供项目运行所需的依赖,比如android的sdk。一般不建议使用,因为该依赖可能只有特定系统提供,那样在别的系统上就无法运行,代码的可移植性较差。
源代码中引入相关类不会报编译错误,但运行时如果加载类就会报ClassNotFound异常。
这是有一个坑,如果新手不熟悉Maven的作用域,且项目某个依赖被设置为providedd(比如在网上复制了别人的配置文件的依赖过来),就会出现一种情况,项目明明可以正常编译,但是运行的时候抛出一个ClassNotFound异常。
下面用provided作用域做一个demo来展示下依赖的作用域。
项目中引入servlet依赖,作用域设置为provided。
代码中初始化servlet的Cookie类,可以正常编译,但是运行的时候会报ClassNotFound异常。
测试代码可以正常使用Cookie类。
作用域和什么时候会引入的关系如下:
编译时 |
运行时 |
测试时 |
|
compile |
√ |
√ |
√ |
test |
× |
× |
√ |
runtime |
× |
√ |
√ |
provided/system |
√ |
× |
√ |
作用域的传递,如果a项目依赖于b项目,作用域为x, b项目依赖于c项目,作用域为y,那么a项目对c项目的依赖的作用域z取决于x和y。下面这个表格描述了不同的x和y产生的z。其中列是x的值,行是y的值。
作用域的传递,如果a项目依赖于b项目,作用域为x, b项目依赖于c项目,作用域为y,那么a项目对c项目的依赖的作用域z取决于x和y。下面这个表格描述了不同的x和y产生的z。其中列是x的值,行是y的值。
compile |
provided |
runtime |
test |
|
compile |
compile(*) |
- |
runtime |
- |
provided |
provided |
- |
provided |
- |
runtime |
runtime |
- |
runtime |
- |
test |
test |
- |
test |
- |
注:官方文档对compile(*)解释是这样的,这个一般情况下应该是runtime,但是对于一种特使情况:b的类继承了c中某个类或者接口,为了能够编译通过,就需要设置作用域的值为compile。
可以用几句话概述:
1.当a对b的依赖的作用域是provided或者test的时候,不会传递任何依赖。
2.当a对b的依赖是作用域是compile或者runtime的时候,a对c的依赖的作用域由b对c的依赖的作用域确定,但是compile一般会被降级为runtime。
3.特殊情况下a对c会的依赖的作用域会是compile。需要满足几个条件:
(1)a对b的依赖的作用域是compile
(2) b对c的依赖的作用域是compile
(3)b的类继承了c中的某个类或者接口。
Optional
Optional的使用场景官方文档中是这样描述的:一个项目A提供了某些特性,这些特性有时不会被用到,且这些特性无法被划分到子模块中,而这些特性又需要使用到一些特定的依赖比如B。那么这项目就可以把这些依赖的optional的值设置为true。官方文档中的Demo是这样的:
...
sample.ProjectB
Project-B
1.0
compile
true
现在有个项目X依赖于A,记作,虽然A的pom中声明了对Project-B的依赖的作用域是compile,但是A不会传递对B的依赖,如果X需要使用到A中的这部分特性,那么需要在pom文件中显式声明对B的依赖。
例子:比如一个ORM框架,支持连接不同的数据库mysql、Oracle、SqlServer,这个框架就对这些数据库的jdbc库有一个依赖,但是别的项目使用这个框架时候一般只用连一种数据库,为了避免引入不必要的库,框架的pom文件中就需要声明对jdbc库的依赖为optional的。
上面这个例子是站在项目A的角度考虑的,但是有些时候A并没有把对B的依赖声明optional的,可能是因为依赖于B的特性大部分时候都有用到,为了使用者方便,就没有声明,也可能是别的原因。
此时站在项目X的角度考虑,它需要使用到项目A。但是不用使用到依赖于B的一些特性,所以不需要依赖于项目B,同时因为一些原因,X需要排除掉对B的依赖比如B有一些安全问题、或者是为了节省项目空间。这个时候X就需要使用到exclusions来排除对B的依赖。官方给出的DEMO如下:
...
sample.ProjectA
Project-A
1.0
compile
sample.ProjectB
Project-B
如果一个项目有多个模块,每个模块都有一个共同的依赖,如果在多个模块中声明这个依赖的版本,这样不论是管理和维护都很麻烦,所以maven提供了一种集中化依赖配置信息的方法:dependencyManagement。下面用官方的Demo来说明dependencyManagement的使用。
dependencyManagement元素中可以声明关于依赖的配置信息,但是不会实际引入引来,如果一个依赖在dependencyManagement已经进行了有关的的配置,那么实际引入依赖的时候就可以简化相关的配置信息。
同时加上maven中子POM可以继承父POM的配置信息这个特性,就可以使用dependencyManagement达到配置信息集中化的效果。
假设项目A的依赖如下:
...
group-a
artifact-a
1.0
group-c
excluded-artifact
group-a
artifact-b
1.0
bar
runtime
项目B的依赖如下:
...
group-c
artifact-b
1.0
war
runtime
group-a
artifact-b
1.0
bar
runtime
如果它们有同一个父POM,那么可以在父POM中这么声明:
...
group-a
artifact-a
1.0
group-c
excluded-artifact
group-c
artifact-b
1.0
war
runtime
group-a
artifact-b
1.0
bar
runtime
那么项目A和项目B的配置就可以简化成下面这样:
项目A
...
group-a
artifact-a
group-a
artifact-b
bar
项目B
...
group-c
artifact-b
war
group-a
artifact-b
bar
因为在父POM的dependencyManagement元素下声明了依赖的版本和起来配置属性,A和B只需要声明依赖的groupId、artifactID以及type就够了。所以dependencyManagement起到了一个集中化依赖配置的作用。
依赖管理简化还有一个作用是确定传递依赖的版本。
官方的DEMO如下:
项目A的配置如下:
4.0.0
maven
A
pom
A
1.0
test
a
1.2
test
b
1.0
compile
test
c
1.0
compile
test
d
1.2
项目B用项目A作为父POM,且它的依赖配置如下:
A
maven
1.0
4.0.0
maven
B
pom
B
1.0
test
d
1.0
test
a
1.0
runtime
test
c
runtime
经过上面的声明配置,当maven在项目B上运行时,最后采用的a、b、c、d的版本都是1.0。
需要注意,确定传递依赖的版本不同配置的方式优先级如下:
直接声明 > dependencyManagement > 依赖调解
(1)a、c在b中显式声明为1.0,按照依赖调接规则,显式声明拥有最高的优先级,所以a、c为1.0版本。
(2)b在B的父POM A中的dependency management项中取1.0版本,而dependency management相对依赖调解有更高的优先级,所以b最终取1.0版本。
(3)B的POM中没有显式声明对d的依赖,所以d可能是a或者c的传递依赖。同样的规则,dependency management相对依赖调解有更高的优先级且项目当前POM文件相对父POM文件有更高的优先级,所以d最终取1.0版本。
前面介绍dependency management可以集中化项目的依赖配置,但是项目需要声明特定的父POM,这样就限制很大,只能引入一个项目的dependency management,所以这里就需要使用作用域为import的依赖。
官方的Demo如下:
项目B的配置如下:
4.0.0
maven
B
pom
B
1.0
maven
A
1.0
pom
import
test
d
1.0
test
a
1.0
runtime
test
c
runtime
B在dependencyManagement引入了项目A,type为pom,scope为import。(注意import只能在dependencyManagement下使用,且搭配的type只能为pom)。项目A的配置文件和前一个例子的项目A一样,而这个项目B依赖的a、b、c、d的版本同样都是1.0。所以
引入pom和父POM的效果一样。
引入POM配置的过程是递归的,加入POM A引入了POM B,POM B引入了POMC,那么POM A也会引用POM C的配置。
如果dependencyManagement下引入了多个pom,那么先声明的具有更高的优先级。
dependencyManagement配置的优先级也采取和依赖调解一样的最短路径优先原则,即如果存在多个相同的配置,则取最短路径的的配置,如果有路径长度相同,则取先声明的。
有时一个库下面有多个构件,使用的时候构件版本通常有关联,比如使用1.1 A的时候,就需要使用1.2版本的B(通常A、B是一个项目下不同的模块)。比如引入Spring Boot库的时候就有这种情况。
就会有这种情况,如果别的项目需要单独确定A、B的版本,那么这样对使用和维护都很麻烦。所以Maven提供了BOM来简化这种情况下项目的POM配置。
BOM(Bill of Materials)一般是库的提供者进行配置,使用者只需要引入这个BOM就可以很方面的引用库下的构件。
官方给出的DEMO如下:
假设有一个库,库下有两个构件project1、project2,通过BOM文件来简化其它项目引入该库时的配置。
首先定义一个BOM:
4.0.0
com.test
bom
1.0.0
pom
1.0.0
1.0.0
com.test
project1
${project1Version}
com.test
project2
${project2Version}
parent
然后是声明一个父POM,引用这个BOM作为父POM,项目下有两个模块
4.0.0
com.test
1.0.0
bom
com.test
parent
1.0.0
pom
log4j
log4j
1.2.12
commons-logging
commons-logging
1.1.1
project1
project2
project1和project2。
4.0.0
com.test
1.0.0
parent
com.test
project1
${project1Version}
jar
log4j
log4j
4.0.0
com.test
1.0.0
parent
com.test
project2
${project2Version}
jar
commons-logging
commons-logging
然后其它项目要引入这个库时就可以通过引入BOM来简化配置。
4.0.0
com.test
use
1.0.0
jar
com.test
bom
1.0.0
pom
import
com.test
project1
com.test
project2
Maven使用依赖调解自动解决依赖版本冲突的问题。
通过依赖的作用域、optional、exclusion,可以避免不必要的依赖传递。
通过depedencyManagement,可以集中化依赖的配置信息。
通过BOM,库的提供者可以让使用者简化引入库时的配置。