Maven通过pom文件来管理Java包的依赖关系,多个pom文件组合在一起可以理解为一颗抽象的依赖树,父节点或祖父节点的包会传递给子节点或祖孙节点,这被称作传递依赖(Transitive-Dependencies)。
传递依赖一般通过两个标签来实现:parent与dependency,其中,parent定义了上级依赖,dependency定义了下级依赖。
随着项目复杂性的提升,依赖树中包发生冲突的概率也会增加,Maven通过依赖仲裁(Dependency-Mediation)与依赖管理(Dependency-Management)来唯一确定依赖树中包的版本。
一、依赖仲裁
Maven使用dependencies标签来引入使用包。
依赖仲裁,简而言之,就是在依赖树中包的版本发生冲突时,选择哪一个包的过程,Maven采取就近原则,即:
- 首先选择依赖层级最浅的
- 若依赖层次一致,则选择最先定义的
如对于以下依赖树:(参见官网)
A
├─ B
│ └─ C
│ └─ D(2.0)
└─ E
└─ D(1.0)
D包有两个版本,其对应了两条依赖路径:A –> B –> C –> D 与 A –> E –> D,第二条路径最短(依赖层级最浅),符合上述就近原则的第一条规则,最终选择D包的1.0版本。
又如,对于以下依赖树:
A
├─ B
│ └─ D(2.0)
│
└─ C
│ └─ D(1.0)
D同样有两个版本,他们的依赖层级一样,A –> B –> D路径上的D最先定义,符合上述就近原则的第二条规则,最终选择D包的2.0版本。
如果想要某个包的版本优先使用,可以将节点提到上级pom节点(满足依赖层级最浅的规则),也可以将对应节点交换位置(满足最先定义的规则)。
当然,还有另外一种方法,那就是依赖管理。
二、依赖管理
Maven使用dependencyManagement标签来定义包。
与dependencies不同的地方在于,dependencyManagement中的包只是一个定义,只有在dependencies中引入对应包时,dependencyManagement中的定义才有意义。
凡是在dependencyManagement中定义的包,dependencies中引入对应包时,都可以省略版本号,Maven会自动从dependencyManagement中找到对应版本,这一特性为包的版本统一提供了极大的便利性。
同时,有一点需要非常注意:在子模块中,dependencies中包的版本无法覆盖dependencyManagement中对应包的版本,如下所示:
<project>
...
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.9.3</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>
</dependencyManagement>
...
</project>
在上述pom中dependencies与dependencyManagement使用了okhttp,但它们的版本不一样,此时,Maven将优先使用在dependencyManagement中定义的3.8.1版本,因为在Maven中,依赖管理优先于依赖仲裁 ,即使是在父节点(根节点除外)的dependencies引入一个层级更浅的okhttp版本,这里依旧会取dependencyManagement中okhttp的版本,因为不管okhttp位于哪个节点(根节点除外),它都是处于依赖仲裁的范围内,它的优先级永远低于依赖管理。
同时,顶层根节点(如启动类所在的web节点)的优先级最高。
因此,在Maven依赖中存在以下优先级:
顶层根节点引入的依赖 > 依赖管理 > 依赖仲裁
三、总结
至此,对于如何唯一确定一个包,并解决包冲突,我们也有了明确的方向:
- 根据依赖管理原则,在dependencyManagement中寻找包的版本
- 根据依赖仲裁原则,在dependencies中寻找包的版本
- 在顶层根节点寻找包的版本
- 按照上述
顶层根节点引入的依赖 > 依赖管理 > 依赖仲裁
找到合适的包版本
参考文档