管理项目依赖是 Maven 的重要核心功能之一,为了能自动化地解析任何一个 Java 构件,Maven 需要将它们进行唯一标识,这就是 Maven 依赖管理的底层基础 —— 坐标。
Maven 坐标
Maven 中,任何一个构件必须明确定义自己的坐标,而一组 Maven 坐标是通过一些元素定义的,它们是 groupId、artifactId、version、packaging 和 classifier。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.18.RELEASE</version>
<packaging>jar</packaging>
</dependency>
- groupId:指项目组织唯一的标识符,常以 Java 的包路径的前缀表示,如上例中,org.springframework 是 Spring 框架下所有项目的前缀,其下包括 spring-web、spring-context 等模块;
- artifactId:指项目的唯一的标识符,常以项目根目录的名称表示,该元素定义实际项目中的一个 Maven 项目(模块);
- version:该元素定义项目当前所处的版本,Maven 有一套版本规范,主要是快照(SNAPSHOT)和正式版本(RELEASE);
- packaging:该元素定义项目的打包方式,是可选项,默认是 jar,即打包后生成 .jar,其它常用的有 war,此时打包后生成 .war 形式的文件;
- classifier:该元素用来帮助定义构件输出的一些附属构件,附属构件与主构件对应,如常见到的附带的 javadoc 文件和 sources 文件;需要注意的是,classifier 是不能直接定义的。
依赖管理
依赖配置
完整的依赖配置,除了坐标的相关的元素之外,还包括如下其它配置项:
<project>
...
<dependencies>
...
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>
<groupId>...</groupId>
<artifactId>...</artifactId>
</exclusion>
</exclusions>
</dependency>
...
<dependencies>
...
</project>
- groupId、artifactId 和 version:依赖的基本坐标;
- type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值是 jar;
- scope:依赖的范围;
- optional:标记依赖是否可选,默认是 false;
- exclusions:用来排除传递性依赖。
依赖范围
首先需要知道,Maven 在编译项目时使用一套 classpath;依赖范围就是用来控制与三种 classpath(编译 classpath、测试 classpath 和 运行 classpath) 的关系。
Maven 有以下几种依赖范围,即 scope
元素对应的可选值:
- compile:编译依赖范围,默认使用该依赖范围;该依赖范围对于编译、测试和运行都有效;
- test:测试依赖范围,该依赖范围只对于测试 classpath 有效,典型的例子是 JUnit;
- runtime:运行时依赖范围,此依赖范围对于测试和运行都有效;
- provided:已提供依赖范围,此依赖范围对于编译和测试有效,但在运行时无效;典型的例子是 servlet-api,编译和测试时需要,但在运行时由于容器已提供,就不需要重复引入;
- system:系统依赖范围,使用时,必须依赖 systemPath 元素显性地指定依赖文件的路径,有效范围与 provided 一样;由于此类依赖不是通过 Maven 仓库解析的,往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用;此外,systemPath 元素可以引用环境变量,如 ${java.home} 等。
- import:导入依赖范围,从 Maven 2.0.9 开始支持,此依赖不会对三种 classpath 产生实际影响。
依赖范围(scope) | 编译 classpath | 测试 classpath | 运行 classpath | 例子 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | - | Y | - | JUnit |
runtime | - | Y | Y | JDBC 驱动实现 |
provided | Y | Y | - | servlet api |
system | Y | Y | - | 本地的,maven 仓库意外以外的 |
更多信息可以参考 Maven scope 依赖范围作用及使用详解。
依赖传递与冲突调解
当依赖的构件也依赖其它构件时,它上游的其它依赖也加入进来,如下:
A -> B -> C
当依赖构件 A 时,由于 A 依赖 B,B 又依赖 C,所以依赖构件 A 会最终也将 B 和 C 加入到依赖里。
Maven 引入的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下,我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有时候,当传递性依赖造成不同版本的依赖同时引入时,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。
假设项目 A 有如下依赖关系:
A -> B -> C -> X(1.0) A -> D -> X(2.0)
根据依赖传递性,A 项目在两条路径上依赖两个版本的 X,那应该选择依赖哪一个呢?
Maven 依赖冲突的调解第一原则是路径最近者优先,因此如上示例会选择第二条路径的 X(2.0) 依赖。
依赖调解第一原则不能解决所有冲突问题,如下示例:
A -> B -> Y(1.0) A -> C -> Y(2.0)
Y(1.0) 和 Y(2.0) 的路径长度一样,在 Maven 2.0.9 版本开始之后,它定义了第二原则是第一声明者优先,即在 pom 中谁先声明谁先被采用。
可选依赖
如果当前的依赖项不想进行传递,可以将上游依赖项的 optional 设置为 true,示例如下:
A -> B -> C
假设 A 依赖 B 和 C,如果 B 和 C 的 optional 设置为 true 时,当有项目依赖 A 时,它不会引入 B 或 C 依赖,即 B 和 C 失去传递性。
排除依赖
传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。
可以使用 exclusions 元素中指定要排除的 maven 构件,每个都放在 exclusion。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.0.9.RELEASE</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>