一、背景和起源
依赖管理是Maven的一个核心功能。管理单个模块项目的依赖相对比较容易,但是如果是多模块项目或者有几百个模块的项目就是一个巨大的挑战。
如果手动构建项目,那么就先需要梳理各个模块pom中定义的依赖和版本,然后进行下载到本地,并且还需要依赖中pom定义的依赖和版本进行下载,如此反复。这中间需要解决相同依赖版本不同的情况、一些依赖需要排除等。
二、依赖配置
项目对外部库的依赖,可以配置在pom文件的dependency节点,需要提供依赖的groupId,artifactId,version。例如:
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>3.2.2</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
</exclusions>
</dependency>
三、传递依赖
传递依赖机制是指项目只需要在POM中定义直接依赖、不需要定义任何间接依赖。Maven会读取各直接依赖的POM,将必要的间接依赖引入到当前项目。传递依赖机制简化了POM的配置,也将开发者从依赖的复杂传递关系中解脱出来。
1.依赖仲裁机制
当项目出现多版本依赖时,maven就需要通过依赖仲裁机制决定选择哪个版本依赖。
- 路径优先:依赖层级越浅优先越高,层级越深优先级越低
- 声明优先:当依赖所在层级相同,声明靠前的依赖优先级高于声明靠后的优先级
2.依赖范围
依赖范围会限制一些依赖在传递依赖机制中的传递,也就是一些间接依赖可能不会引入到当前项目中。传递范围Scope主要分为六种:
- compile: 编译期,scope默认范围。也就是从编译期直到运行期都需要此依赖。
- provided: 表示运行容器或者jdk提供的依赖,只需要在编译期引入此依赖,打包成运行程序时不需要包含此依赖。
- runtime: 表示测试和运行期需要引入此依赖,编译期不需要此依赖,但打包时需要包含此依赖。
- test: 测试期需要引入此依赖,编译期和打包时都不需要包含此依赖
- system: 与provided类似,运行期间由用户指定的系统路径提供依赖
- import:用于引入外部定义的依赖版本管理文件,相当于将依赖版本声明在pom的dependencyManagement节点。
以上依赖范围的依赖在整个构建和运行期间起作用范围如下:
3.依赖范围对传递的限制
项目A的pom中定义依赖项目B并且Scope为X,项目B中pom中定义依赖项目C并且Scope为Y。根据Maven传递依赖的机制,项目A不仅会加载直接依赖B还会加载间接依赖C,但是以上Scope X和Y会对是否加载依赖C以及依赖C的作用范围产生影响。
传递性依赖范围影响受到Scope X和Scop Y的影响如下:
注意:列为Scope X,行为Scope Y,交叉部分为项目A中针对依赖C的作用范围,当表格中为‘-’时,表示项目A不会引入依赖C。
4.排除依赖
如果当前项目只想引入直接依赖和部份间接依赖,这样需要将某个间接依赖排除掉,这样可以通过设置exclusions来实现。
比如以下例子就是排除间接依赖guava包。
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-mapreduce-client-core</artifactId>
<version>3.2.2</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
5.可选依赖
如果当前项目在被依赖时,默认情况下想将当前项目的直接依赖不被加载可以用可选依赖。功能相当于当前项目被引用时配置了exclusions节点。
比如以下例子就是排除间接依赖guava包。
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<!-- 配置不透明 -->
<optional>true</optional>
</dependency>
四、依赖集中管理
pom中dependencies节点可以定义项目需要引用的依赖,如果是一个多module项目,那么每个module中pom定义的依赖可能出现版本不一致,可能会出现版本冲突并且在升级版本时也不方便集中管理。所以可以在pom中的dependencyManagement节点对依赖的版本、范围、排除项等进行集中管理,这样整个项目中的版本可以保持一致并且方便进行版本管控。
1.父pom集中管理
当项目中有多个module时,可以在项目pom的dependencyManagement节点对依赖进行集中管理,来决定引入依赖的版本、排除间接依赖、依赖范围等。module中的pom将项目pom作为父pom,module中的pom只定义dependencies节点来决定引入哪些依赖。
1.1 不使用dependencyManagement对接点进行管理
如果项目不是用dependencyManagement管理,则两个项目的pom可以如下进行配置,其中依赖必须包含{groupId, artifactId, type}。
Project A POM:
<project>
...
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
Project B POM:
<project>
...
<dependencies>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
1.2 使用dependencyManagement对接点进行管理
为了简化和统一管理,可以将以上两个项目所有的依赖版本管理统一放到父pom中,这样两个项目中依赖定义可以只定义 {groupId, artifactId}。
Parent Project POM:
<project>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
两个项目中的pom配置可以简化为:
Project A POM:
<project>
...
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>bar</type>
</dependency>
</dependencies>
</project>
Project B POM:
<project>
...
<dependencies>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>war</type>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>bar</type>
</dependency>
</dependencies>
</project>
2.当前pom和父pom一起集中管理
依赖集中管理也支持传递依赖,也就是集中管理当pom和父pom都配置了,这两个都会对依赖起到管理作用。
例如Project B继承了Project A,并且两个都定义了依赖管理。
Project A POM:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>A</artifactId>
<packaging>pom</packaging>
<name>A</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Project B POM:
<project>
<parent>
<artifactId>A</artifactId>
<groupId>maven</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
当构建项目B时,依赖a、依赖b、依赖c 都是版本1.0被使用。
- 依赖a:由于在项目B中直接定义了版本号和scope,所以最终是加载的依赖a版本号为1.0、scope为runtime。
- 依赖c:由于在项目B中只定义了scope,所以版本号需要由dependencyManagement节点决定,所以最终是加载的依赖c版本号为1.0、scope为runtime。
- 依赖b:如果依赖b被依赖a或者依赖c间接引用,那么依赖b的版本由父pom中dependencyManagement节点决定,所以最终是加载的依赖b版本号为1.0、scope为compile。
- 依赖d:因为项目B和父pom都在dependencyManagement节点定义了依赖d,所以管理节点是项目B中为准。
3.引入外部集中管理
如果项目module过多,没办法继承同一个父pom。这样可以通过引入一个外部的pom,这个pom只有dependencyManagement节点。
五、依赖版本确定流程
依赖版本的查找与依赖传递的解决机制都是基于项目Pom,也就是当项目有多个module时,是会按照每个module的pom为基本单位进行依赖的查找和传递。
例如:有父项目ProjectA,下边有子项目ProjectB和ProjectC。子项目ProjectC依赖ProjectB。
Project A POM:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>A</artifactId>
<packaging>pom</packaging>
<name>A</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Project B POM:
<project>
<parent>
<artifactId>A</artifactId>
<groupId>maven</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>2.4</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>2.5</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>2.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>3.6</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>3.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
Project C POM:
<project>
<parent>
<artifactId>A</artifactId>
<groupId>maven</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>2.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>3.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
1.配置优先级
1.1 不同节点优先级
同一个Pom中dependencies优先级高于dependencyManagement。比如在Project C中dependencies中定义了依赖a的版本和scope,所以会忽略dependencyManagement中依赖a的版本和scope。依赖a会采用版本3.0。
1.2 不同pom中dependencyManagement优先级
父子Pom中子Pom中dependencyManagement优先级高于父Pom中的dependencyManagement。比如在Project B中dependencies中没有定义了依赖c的版本,并且dependencyManagement定义了依赖c版本为2.1, 所以会忽略ProjectA中dependencyManagement中依赖c的版本和scope。
1.3 重复定义优先级
dependency在同一个dependencies或dependencyManagement中,如果出现相同{groupId,artifactId},则后出现的会覆盖先出现。
比如在Project B中dependencies和dependencyManagement中重复定义了依赖a,所以在各自节点中,起作用的都是后定义的版本。按照目前Pom,依赖a会是3.7,如果删除dependencies对依赖a的定义,则依赖a会采用版本2.5。
2.版本确定流程
2.1 对Pom的dependencies中定义的{groupId,artifactId}组合顺序遍历,如果有相同组合以后定义的信息为主。
2.2 针对以上去重后每个租户,如果version和exclusions都明确定义了,则确定了此依赖。
2.3 如果version和exclusions有一个没有定义,如果本Pom中dependencyManagement中定义了此依赖信息,则进行加载此依赖。
2.4 如果本Pom中dependencyManagement中没有定义了此依赖,则去父Pom的dependencyManagement中查找此依赖定义,如果没有会一直查找到默认Pom文件对应节点
2.5 将依赖的Pom的dependencies按照 2.1进行处理后,还需要去掉exclusions的租户后跳转到2.2。
2.6 如果加载完成所有依赖,出现了相同{groupId,artifactId},则需要根据仲裁机制的路径优先和声明优先进行选择。
3.版本冲突
maven有完善的版本管理和冲突解决机制,但是为什么实际工程中还会出现依赖多版本和冲突问题。主要是因为maven的冲突仲裁机制是以Pom为单位进行的,如果一个项目有很大module,而对依赖版没有进行统一管理,就会出现相同的依赖在不同module的版本不一致问题。
例如上边的例子依赖a在Project B中引入的是2.5版本,在Project C中引入的是3.0版本。所以针对整个项目在运行过程中可能会优先加载2.5版本,这就导致Project C中调用依赖3.0版本a的一些方法缺失或者实现有差异,导致功能异常。
3.1 版本统一管理
将所有版本管理统一到父Pom的dependencyManagement中,子项目中不要配置dependencyManagement。
3.2 排除依赖
可以在子项目定义依赖时通过exclusions将一些间接依赖排除掉,解决版本冲突。
总结
主要是对maven依赖传递机制的介绍、依赖管理的的配置、依赖版本和范围确定机制、依赖冲突解决等。
参考
1.maven文档-依赖机制