何为依赖
一个软件系统中的两个或多个模块、组件或对象之间的关系,其中一个模块(被称为被依赖项或依赖对象)需要另一个模块(被称为依赖项或依赖)来完成某些功能或任务。
软件开发效率得以提高的关键在于复用,可以站在巨人的肩膀上,可以将已经存在的工具包直接拿来使用,不用重复造轮子,这些被直接拿来使用的工具包就是「依赖」。
何为坐标
「依赖」有了之后,那怎么使用呢?在没有引入项目管理工具之前,通常是通过「复制/粘贴」方式把需要用到的依赖拷贝到项目目录下,这样做既麻烦又低效。于是相关的依赖管理工具便应运而生,而 Maven 就是其中比较优秀的一个。
为了解决使用依赖的麻烦以及低效的问题,首先就需要有一套标准化的使用和管理流程,而「坐标」就是 Maven 的依赖使用和管理的底层基础。
在 Maven 的世界中,所有的依赖都可以通过坐标来定位,坐标最基础的三个元素是:groupId,artifactId,version。绝大部分情况下,只要通过这三个坐标元素就可以定位一个依赖了。
依赖配置
了解了依赖和坐标的概念之后,我们就可以开始学习怎么使用坐标来配置依赖了。一个依赖的声明可以包含以下元素:
<project>
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<classifier>...</classifier>
<scope>...</scope>
<optional>...</optional>
<systemPath>...</systemPath>
<exclusions>
<exclusion>
<groupId>...</groupId>
<artifactId>...</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependencies>
</project>
- groupId(实际项目),artifactId(模块名),version:基本元素,必需值,否则无法定位依赖。
- type:依赖类型,对应项目的 packaging 值。大部分情况无需配置,默认值为 jar。
- classifier:附属构建扩展坐标。
- scope:依赖范围,用来控制依赖生效的 classpath 范围。
- exclusions:排除传递性依赖。
- optional:依赖是否可选,值为 true 或者 false,默认为 false。当配置成 true 时,此依赖不会被传递。
我的理解:1、2、3 关注的是使用哪个依赖,4、5、6 关注的是如何使用。
接下来详细了解一下 3、4、5。
classifier
该元素用来帮助定位一些附属构建,例如主构件是 spring-web-6.0.11.jar,在生成主构件的同时可以通过插件生成 spring-web-6.0.11-javadoc.jar,spring-web-6.0.11-sources.jar 两个附属构建,javadoc、sources 就是这两个附属构建的 classifier。当然,这两种类型的包一般不会当做依赖引入。
还有一种常见的用法,当某一个项目的构建需要兼容不同版本的 jdk 时,打包是会生成多个构建,例如这样:json-lib-2.2.1-jdk9.jar、json-lib-2.2.1-jdk17.jar。其中的 jdk9,jdk17 就是 classifier,而且像这种类型的包必须指定 classifier,只通过三个基本元素是定位不到的。
scope
scope 用来规定依赖与三种 classpath 的关系,分别是:编译 classpath,测试 classpath,运行时 classpath。
这三种 classpath 分别也对应着 maven 中主程序编译,测试用例编译、执行,主程序打包这三个流程。
设置 scope 是为了更精确的控制依赖生效的 classpath 范围,使引入的依赖在该生效的 classpath 中生效,在不该生效的 classpath 中无效。这样才能避免最终运行的程序中出现依赖冗余或者缺失的情况。
scope 类型
依赖的 scope 的类型一共有 6 个,常用的是前 4 个,每一个 scope 的类型控制依赖会在某一个或者某几个 classpath 中生效。
- compile:默认值,此类型的依赖在主程序编译,测试用例编译、运行生效,项目打包时也会打进包里(也就是运行时 classpath)。
- test:只在测试用例编译、执行时生效,主程序编译及项目打包时都不会生效。
- provided:主程序编译,测试用例编译、执行时生效,但是程序打包时不会生效,也就是不会被打进包里,因为程序运行时的环境会提供此类型的依赖,打进包里反而可能出现依赖冲突的情况。
- runtime:主程序编译、测试用例编译时不会生效,测试用例执行以及主程序运行时会生效。
- system:同 provided,只不过使用时必须通过
<system-Path>
元素显式指定依赖文件路径,一般与本机强绑定,可能造成构建的不可移植,应谨慎使用。 - import:用来导入其他 pom 类型的依赖,不实际引入 jar 依赖,只用在 dependencyManagement 元素中。
简洁版的对应关系描述可以看下面的表格。
scope 与 classpath 关系
依赖范围(scope) | 编译 classpath 有效 | 测试 classpath 有效 | 运行时 classpath 有效 | 例子 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | N | Y | N | JUnit |
provided | Y | Y | N | servlet-api |
runtime | N | Y | Y | JDBC 驱动 |
system | Y | Y | N | 本地的,Maven 仓库之外的类库文件 |
exclusions
要理解 exclusions 这个元素,需要先了解下什么是传递性依赖,然后再理解为什么要排除传递性依赖。
传递性依赖
举个例子,项目 A 依赖 spring-web-5.2.8.RELEASE.jar,而 spring-web-5.2.8.RELEASE.jar 又依赖 spring-core-5.2.8.RELEASE.jar,如果要保证项目 A 能正常运行就需要把 spring-core-5.2.8.RELEASE.jar 这个依赖也添加进来,一旦依赖多了之后就会非常麻烦。Maven 的传递性依赖机制就解决了这个问题,每个项目只需要关注自己的直接依赖,间接依赖会通过传递性依赖的方式自动引入。
传递性依赖范围
依赖范围不仅可以控制依赖与三种 classpath 的关系,还对传递性依赖范围产生影响。举例,A 依赖 B,B 依赖 C,A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖。第一直接依赖范围和第二直接依赖的范围决定了传递性依赖的范围。如下表,最左边一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围。 --《Maven 实战》- 许晓斌
compile | test | provided | runtime | |
---|---|---|---|---|
compile | compile | - | - | runtime |
test | test | - | - | test |
provided | provided | - | provided | provided |
runtime | runtime | - | - | runtime |
排除传递性依赖
为什么要排除传递性依赖?简单来说,有时候传递性依赖并不是我们需要的,并且还有可能导致我们想要的功能实现不了。举个例子,spring-boot-starter-logging 默认使用的是日志框架是 logback,当我们想换成 log4j2 的时候,单单引入 log4j2 的依赖并不行,因为 spring 的逻辑是只要在 classpath 中存在 logback 的包,会优先使用 logback。所以,如果我们想用 log4j2 替换掉 logback,只能使用 exclusions 将 logback 的传递性依赖排除掉,然后再加上 log4j2 的依赖。
依赖调解
得益于 Maven 的传递性依赖的机制,大大减少了声明依赖的操作,但是当发生依赖冲突时,我们需要知道如何去解决,解决的首要条件就是得知道传递性依赖是从哪条路径引入进来的。Maven 的依赖调解有两大原则:
- 第一原则:路径最近者优先。
- 第二原则:第一声明者优先。
例 1,有 A -> B -> C -> X(1.0) 和 A -> D -> X(2.0) 两条依赖关系,X 是 A 的传递性依赖,这个时候两条路径都有 X 这个依赖,根据第一原则,路径短的传递性依赖会生效,也就是 2.0 版本的 X 会成为 A 项目的最终传递性依赖。
例 2,有 A -> B -> Y(1.0) 和 A -> C -> Y(2.0) 两条依赖关系,根据第一原则,两个版本的 Y 依赖路径长度相同,无法调解,这个时候就需要用到第二原则,先声明的生效,也就是说在项目的 pom 文件中先定义的依赖先生效,比如 B 声明在 C 前面,那生效的传递性依赖就是 1.0 版本的 Y,反之则是 2.0 版本的 Y。
最佳实践
在了解的 Maven 依赖相关的内容之后,这里有几条实践的建议供大家参考:
- 排除 snapshot 版本的依赖,只使用 release 版本
- 归类依赖,把来自同一项目的不同模块的依赖版本号统一定义在一个地方,其他地方直接引用
- 优化依赖,将 Used undeclared dependencies 显示声明,Unused declared dependencies 仔细检查,确实无效的可以删除,但是一定要谨慎测试验证。