8. 依赖管理
Maven 的依赖管理系统是其核心功能之一,通过简化项目依赖的配置和管理,确保构建过程中的依赖关系可以自动解析、下载和更新。
8.1 依赖管理
在 Maven 中,BOM(Bill of Materials)是一种特殊的 POM 文件,通常用于集中管理一组相关依赖的版本。BOM 的作用是提供依赖版本的统一管理,从而保证项目中的依赖版本一致,避免不同版本带来的兼容性问题。通过 BOM,可以确保所有项目使用的依赖版本一致,减少因依赖版本不同而产生的问题。项目在使用 BOM 后,不需要在子模块或子项目中重复指定依赖的版本信息,只需在 BOM 中管理这些版本即可,减少了代码的冗余。
BOM 文件主要通过 <dependencyManagement>
元素来管理依赖,通常作为一个父 POM 提供给其他项目使用。项目通过 scope=import 方式引入 BOM 依赖,本质就是导入父 POM 中的 <dependencyManagement>
。<dependencyManagement>
能够强制指定版本,具有较高的优先级。
使用示例:
<project>
<groupId>com.example</groupId>
<artifactId>example-bom</artifactId>
<version>1.0.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.10</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
在其他项目中引入 BOM 文件:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>example-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
注意:要引入 BOM 文件的话,必须在
<dependencyManagement>
元素里面引入,不能在<dependencies>
里面进行引入!
8.2 依赖引入
Maven 中的依赖引入是项目构建和管理的核心功能之一,它帮助开发者自动管理和下载项目所需的各种库和构件。Maven 通过 <dependencies>
标签来定义依赖,并能根据配置自动解析、下载、管理这些依赖的版本及传递性依赖,确保项目所需依赖在构建时自动可用。
使用示例:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.10</version>
<scope>compile</scope>
</dependency>
</dependencies>
8.3 作用范围
Maven 的作用范围用于控制依赖在项目构建过程中的 使用阶段 及 可传递性。作用范围分为以下几种类型,每种类型的作用范围都影响依赖在编译、测试、运行等不同阶段的可用性:
- compile:(具有传递性)默认范围,编译、测试和运行时可用。
- provided:(没有传递性)编译和测试可用,但不包含在最终打包中,适用于容器已提供的依赖(如 Servlet API)。
- runtime:(具有传递性)测试和运行时可用,编译阶段不可见,常用于驱动程序或数据库连接。
- test:(没有传递性)仅用于测试阶段,编译和运行时不可见。
- system:(没有传递性)本地文件依赖,必须手动指定路径,不会从仓库中下载。
- import:用于导入BOM文件。
scope | 依赖传递性 | 使用范围 |
---|---|---|
compile: | 是 | 编译、测试、运行 |
test: | 否 | 测试 |
runtime: | 是 | 测试、运行 |
provided: | 否 | 编译、测试 |
system: | 否 | 编译、测试 |
8.4 可选依赖
可选依赖是 Maven 的一种依赖配置,用于控制依赖的 传递性。通常,Maven 会自动将一个依赖的依赖传递到下游项目中,但有时我们希望依赖不再自动传递给使用该项目的其他模块。如果一个依赖被标记为可选依赖,Maven 就不会将它传递给下游项目。这样做的原因通常是因为该依赖可能是项目的一个非核心功能,或是只有特定环境或条件下才需要的依赖。
本质上 optional 是阻止具有传递性的作用范围的传递性,因此它只适用于作用范围为 compile 或 runtime 的依赖。
要将某个依赖设置为可选依赖,可以在 <dependency>
标签中加入 <optional>true</optional>
元素:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
8.5 冲突解决
在 Maven 中,依赖冲突是指同一个项目的依赖树中存在多个不同版本的同一依赖库,而 Maven 只能使用其中一个版本。冲突通常发生在依赖关系的不同层次上,比如项目直接依赖和间接依赖的版本不一致时,或者传递性依赖之间的版本不同。在 Maven 中,遇到依赖冲突时,Maven 会自动应用路径最短者优先和第一声明者优先的策略来解决冲突。
8.5.1 自动处理策略
路径最短者优先:Maven 会选择距离当前项目路径最短的依赖版本。例如,假设项目 A 依赖 B,B 依赖 C,C 依赖 X 的 1.0 版本,而 F 依赖 D,D 依赖 X 的 2.0 版本。因为 X 的 2.0 版本更近,则最终会选用 X 的 2.0 版本。
- 依赖链路一:A -> B -> C -> X(1.0)
- 依赖链路二:F -> D -> X(2.0)
- 最终选择:X(2.0)
第一声明者优先:如果依赖路径长度相同,Maven 会选择在依赖树中最先声明的那个版本。例如,假设项目 A 依赖 C,C 依赖 X 的 1.0 版本,而 F 依赖 D,D 依赖 X 的 2.0 版本。这里假设 A 比 F 更先声明,那么这里最终会选择 X 的 1.0 版本。
- 依赖链路一:A -> C -> X(1.0)
- 依赖链路二:F -> D -> X(2.0)
- 最终选择:X(1.0) (A先出现)
Maven 在遇到依赖冲突的时候,会利用 路径最短者优先 和 第一声明者优先 两个策略来自动选择依赖。如果Maven最终选择的依赖不是我们想要的,那么我们就必须去解决这个冲突。
8.5.2 打印依赖关系
要想解决依赖冲突,首先必须清楚依赖冲突的类型。我们可以使用 maven-dependency-plugin 插件去分析依赖,使用 mvn dependency:tree
打印出依赖关系树,然后判断是哪种冲突问题。
打印依赖关系树:
$ mvn dependency:tree
[INFO] --- maven-dependency-plugin:3.1.1:tree (default-cli) @ example-project ---
[INFO] com.example:example-project:jar:1.0-SNAPSHOT
[INFO] +- org.springframework:spring-core:jar:5.3.9:compile
[INFO] | \- org.springframework:spring-jcl:jar:5.3.9:compile
[INFO] +- org.springframework:spring-context:jar:5.3.9:compile
[INFO] | +- org.springframework:spring-aop:jar:5.3.9:compile
[INFO] | +- org.springframework:spring-beans:jar:5.3.9:compile
[INFO] | \- org.springframework:spring-expression:jar:5.3.9:compile
[INFO] +- org.slf4j:slf4j-api:jar:1.7.30:compile
[INFO] \- junit:junit:jar:4.13.2:test
[INFO] \- org.hamcrest:hamcrest-core:jar:1.3:test
8.5.3 执行依赖排除
如果是第1种情况,因为违反了 路径最短者优先 原则,因此我们可以采用 依赖排除 方法来解决。假设还是上面路径最短者优先的那个示例,如果最终是想选择 X 的 1.0 版本,那么可以在依赖 F 中排除 X:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>F</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>com.example</groupId>
<artifactId>X</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
8.5.4 调整依赖顺序
如果是第2种情况,因为违反了 第一声明者优先 原则,那么只需要调整 依赖顺序 即可。假设还是上面第一声明者优先的那个示例,如果最终想选择 X 的 2.0 版本,那么只需要调整依赖顺序,先声明依赖 F 再声明依赖 A 即可:
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>F</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>A</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
8.5.5 调整依赖版本
如果出现版本不兼容的话,那么就可能 ClassNotFoundException
、NoSuchMethodError
、MethodNotFoundException
这类的报错。比如选择旧的版本,那么依赖新版本的代码就无法找到新的类或方法等;如果选择新的版本,因为不兼容导致新版本删除了一些类或方法,也可能出现类或方法找不到的情况。
出现这种情况的话就需要升级或降级版本,也即调整 A 的版本或 F 的版本,比如升级 A 的版本,或者降低 F 的版本,使得 A 的传递依赖 X 和 F 的传递依赖 X 这两者版本兼容。
8.5.6 强制指定版本
除此之外,我们还可以使用 <dependencyManagement>
强制指定使用依赖的某个版本,这对于传递依赖特别有用。这里,我们可以使用依赖管理标签来指定 X 的版本:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>X</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>