在上一篇文章《Maven入门:剖析POM核心与GAVP项目坐标》中,我们了解了Maven和POM的基础。本文将深入探讨Maven最核心的功能之一:依赖管理。我们将学习如何声明项目依赖、理解依赖范围、处理传递性依赖与冲突,并掌握使用 <dependencyManagement>
和BOM(Bill of Materials)进行统一版本控制的高级技巧。
<dependencies>
与 <dependency>
:项目的“原材料清单”
软件项目很少从零开始,我们通常会依赖许多第三方库(如Spring Framework, Apache Commons等)来加速开发。在Maven中,这些外部库通过在 pom.xml
文件的 <dependencies>
块中添加一个或多个 <dependency>
元素来声明。
每个 <dependency>
都需要指定其GAV坐标(groupId
, artifactId
, version
)来唯一定位所需的库。
<project ...>
...
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope> </dependency>
</dependencies>
...
</project>
依赖范围 (<scope>
)
<scope>
元素用于控制依赖在不同构建阶段(编译、测试、运行)的可用性,以及该依赖是否会被打包到最终的构件中。常见的依赖范围有:
compile
:默认范围。依赖对主程序代码和测试程序代码都可见,参与项目打包,并随项目一起部署运行。test
:仅对测试代码可见(如src/test/java
),用于编译和执行测试用例。不参与项目打包,也不会随项目部署。例如JUnit
、Mockito
。provided
:表示该依赖在编译和测试时需要,但假定由运行环境(如Servlet容器Tomcat提供servlet-api
)提供。因此,它不参与项目打包。runtime
:表示该依赖在编译主代码时不需要,但在执行测试和项目实际运行时需要。例如JDBC驱动程序,编译时只需要JDBC API,运行时才需要具体的驱动实现。会参与打包。system
:与provided
类似,但需要明确指定本地文件系统上的JAR路径。这种方式可移植性差,不推荐使用。import
:这是一个非常特殊且重要的范围。它仅能用于<dependencyManagement>
标签中,并且type
必须为pom
。它用于从其他POM(通常是BOM - Bill of Materials)导入依赖管理声明,本身不直接引入依赖,而是引入一套版本管理的“契约”。我们稍后会详细讨论。
传递性依赖 (Transitive Dependencies)
Maven的一大便利之处在于其传递性依赖机制。当你引入一个依赖A,而A又依赖于B,那么B也会自动成为你项目的依赖(除非B的scope是provided
或test
等限制性范围)。这极大地简化了依赖声明,但也可能引入许多你未直接声明的库,并可能导致版本冲突。
- 直接依赖:在当前项目的
pom.xml
中通过<dependency>
明确配置的依赖。 - 间接依赖:由直接依赖所引入的其他依赖。
依赖冲突与排除依赖 (<exclusions>
和 <exclusion>
)
当项目通过不同路径(直接或间接)引入了同一个库的不同版本时(例如,模块A依赖X-1.0,模块C依赖X-2.0),就可能发生依赖冲突。Maven有一套依赖调解(Dependency Mediation)机制来解决这类问题:
- 最短路径优先:层级更浅的依赖声明优先。
- 最先声明优先:如果路径长度相同,则在
pom.xml
中先声明的依赖优先。
尽管有调解机制,但有时我们仍需手动干预。如果不需要某个传递性依赖,或者它导致了难以解决的冲突,可以在直接依赖声明中使用 <exclusions>
标签,并在其中通过 <exclusion>
(指定 groupId
和 artifactId
) 主动断开这个传递依赖。被排除的资源无需指定版本。
<dependency>
<groupId>org.example</groupId>
<artifactId>project-a</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>org.unwanted</groupId>
<artifactId>transitive-lib</artifactId>
</exclusion>
</exclusions>
</dependency>
统一版本控制:<dependencyManagement>
与 BOM
在多模块项目中,确保所有模块使用的公共依赖版本一致至关重要,这能避免因版本不一致引发的各种难以排查的问题,并方便统一升级。<dependencyManagement> 就是为此而生的,它提供了一种“版本锁定”或“推荐版本”的机制。
作用与效果: 在 <dependencyManagement>
中声明的依赖及其版本,并不会被实际引入到项目中。它仅仅是一个“推荐版本清单”或“版本契约”。 当项目本身或其子模块在常规的 <dependencies>
部分声明一个在 <dependencyManagement>
中已定义的库时,可以省略 <version>
标签,Maven会自动采用 <dependencyManagement>
中指定的版本。 当需要变更某个依赖的版本时,只需在父工程(或定义 <dependencyManagement>
的POM)中统一修改一处即可。
与 <dependencies>
的区别:
<dependencies>
:是直接依赖。如果父工程在<dependencies>
中配置了某个依赖,子工程会自动继承这个依赖(除非子工程显式排除或覆盖)。<dependencyManagement>
:是统一管理依赖版本,它本身不会引入任何依赖。子工程若想使用其中声明的依赖,仍需在自己的<dependencies>
中显式引入该依赖(但此时可以省略版本号)。
<project ...>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>
</dependencyManagement>
...
</project>
<project ...>
...
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
...
</project>
BOM (Bill of Materials) 与 <scope>import</scope>
BOM(物料清单)是一种特殊的POM文件(其 <packaging>
通常为 pom
),它的主要目的是在其 <dependencyManagement>
部分集中声明一套经过测试和验证、相互兼容的依赖版本集合。它本身不添加任何依赖到项目中,也不直接参与项目的构建流程(除非被其他POM引用)。
其他项目可以通过在其自身的 <dependencyManagement>
中,使用 <type>pom</type>
和 <scope>import</scope>
来“导入”一个BOM。这样做可以将目标BOM的 <dependencyManagement>
部分的声明“合并”到当前项目的 <dependencyManagement>
中。
为什么需要 <scope>import</scope>
? 想象一下,一个项目可能希望同时采纳多个权威的依赖版本集合(例如,Spring Boot自身提供了一个BOM spring-boot-dependencies
,公司内部也可能有一个通用组件的BOM)。由于Maven项目只能有一个直接父POM,无法通过继承来同时获取多个来源的 <dependencyManagement>
。<scope>import</scope>
完美解决了这个问题。
工作机制:
- 使用位置:必须用在
<dependencyManagement>
部分内部的<dependency>
声明中。 - 声明对象:该
<dependency>
的<type>
必须是pom
,指向你想“参考引用”的BOM文件。 - 效果:Maven会将目标BOM文件里
<dependencyManagement>
定义的所有依赖版本信息,全部“复制”或“合并”到你当前项目的<dependencyManagement>
中。
示例: 假设有一个 spring-versions-bom/pom.xml
:
<project>
<groupId>org.example.boms</groupId>
<artifactId>spring-versions-bom</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
在你的项目根POM my-project/pom.xml
中导入它:
<project>
<groupId>com.myorg</groupId>
<artifactId>my-main-project</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging> <dependencyManagement>
<dependencies>
<dependency>
<groupId>org.example.boms</groupId>
<artifactId>spring-versions-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
</dependencies>
</dependencyManagement>
...
</project>
现在,my-main-project
及其子模块在声明 spring-core
或 spring-boot-starter
依赖时,就可以省略版本号,它们将自动使用 spring-versions-bom
中定义的版本。
yudao-cloud
项目中,根 yudao/pom.xml
就通过 <scope>import</scope>
方式引入了 yudao-dependencies/pom.xml
(一个专用的BOM) 来管理整个项目体系的依赖版本。
系列文章:
- 《Maven入门:剖析POM核心与GAVP项目坐标》
- 当前: 《Maven依赖管理精解:从
<dependencies>
到<dependencyManagement>
与BOM实践》 - 下一篇: 《Maven
<packaging>pom</packaging>
深度解析:父POM、聚合POM与BOM的三重身份》
Comments NOTHING