背景:通过网上的视频课程学习了 Maven 软件工具,在老师讲到 scope 的作用时,产生了一些疑惑(会在下文中指出).。于是,网上查找答案,结果,不尽人意。有些是内容描述不准确,或者不全面,例如没有介绍新添加的 import。有些给出来官方文档,但是却没有全部解释,或者翻译存在问题。 找了好多文章,几乎没有一个是完全没有问题的,而且他们的浏览量是很高。因此,想要写一篇文章更全面、正确的解释 scope 的作用博文供大家参考。(注:本文基于官方文档)。
一 、来自网上的解释
1. 某视频课程中老师的讲解
2. 技术博客文章中的观点
- compile: 默认值,编译、运行、测试都需要或者有效,会被打包
- test: 仅仅参与测试,测试代码的编译和执行需要。另一种说法,仅测试有效 不会被打包
- runtime: 测试, 运行需要参与,不参与项目的编译,会被打包
- provided: 编译、测试都需要。运行存在争议,有说需要的,有说不需要的。容器中会提供,不会被打包
- system: 与 provided 相同,依赖不在仓库,在本地, 需要 systemPath 属性
- import: 导入的范围,它只在使用 dependencyManagement 中,表示从其他 pom 中导入 dependecy 的配置 (因为是后来添加的,只有很少一部分博文介绍了)
3. 以上说法中存在的问题
需要先介绍下 default 生命周期的阶段:
validate
验证项目是正确的,所有必要的信息都是可用的compile
编译项目源代码test
使用单元测试框架测试编译后的源代码package
获取已编译的代码,并将其打包为可发行的格式,例如 JAR。verify
获取已编译的代码,并将其打包为可发行的格式,例如 JAR。install
将包安装到本地仓库,供本地项目使用deploy
将包发布到远程仓库(remote repository),方便其他开发人员和项目共享。
问题 1:编译、测试(这里提及的编译、测试,更准确的说法是编译阶段和测试阶段,因为测试代码也存在编译,所以为了不引起歧义,使用术语编译阶段和测试阶段更好)在 default 生命周期阶段都有对应,但是却不包含运行,有些人把运行也作为了生命周期的一部分。
问题 2:<scope>compile</scope>
在编译、测试时需要(或者有效、参与),编译阶段需要是可以理解的,因为我们的源代码使用了依赖。在测试阶段需要该如何理解呢?容易让人觉得测试源代码也使用了依赖,其实不是这样的。在测试阶段,首先会编译测试代码,然后运行测试代码,测试代码会调用主程序代码(例如,其中的方法),看方法是否能达到预期的功能。在测试代码,调用主程序代码时,主程序代码方法运行,这时会用到依赖。也就是说测试阶段用到依赖,不是测试源代码中直接依赖,而是测试代码运行主程序代码间接使用了依赖。这里需要的定义不统一。
问题 3:我们使用 Mavne 管理项目,开发的项目不止 webapp,还可以开发供其他人使用的 jar 工具包,也可能是桌面应用程序,依赖的包是不会打包进去的。打包的行为只是 webapp 特有的,说 compile 会打包不严谨。
二、官方文档解释
Maven 管理的项目可能是:供其他项目依赖的 jar 工具包(例如,Log4j)、桌面应用程序(例如、我们的 IDEA、Maven 也是程序)、WebApp 等等。
参考官方文档:
Dependency scope is used to limit the transitivity of a dependency and
to determine when a dependency is included in a classpath.
依赖作用域用于限制依赖传递并且控制(或决定)何时把一个依赖添加到类查找路径 classpath。
从这里我们可以得出依赖作用域的主要作用有两个:
- 限制依赖传递
- 控制把依赖添加到 classpath
classpath 类查找路径会在 javac 命令中使用,指出源码依赖的 class 文件的位置,以便可以正常编译源码。另外,会在 java 命令中使用,指出编译好的程序使用的 class 文件的位置,以便可以正常运行。
因此 classpath 在四中情况下会被使用:主程序代码的编译、测试程序代码的编译、主程序代码的运行,测试程序代码的运行。
到目前为止,maven version 3.6,共有六种 scope :
-
compile
默认值,当不指定依赖的 scope 时,使用该值。依赖在项目中的所有 classpath(主程序、测试程序代码的编译和运行)中都可以使用。此外,依赖会传递到依赖的项目。 -
provided
和 compile 很像,但是它表示你期待 JDK 或者容器在运行时提供了此依赖。例如,当构建一个 JaveEE 的 webapp 时,会设置 Servlet API 和相关的 JaveEE APIs 的 scope 为provided
,因为 web 容器提供了这些类。依赖会被添加到编译阶段和测试阶段的 classpath(编译阶段进行主程序代码的编译,测试阶段进行测试代码的编译和运行),但是不会添加到运行时的 classpath(这里的运行仅仅指主程序代码的运行,不涉及测试阶段的运行)。它不会被传递。 -
runtime
编译不需要,但是运行(这里的运行包括主主程序代码和测试程序代码的运行)需要。依赖会被添加到运行时和测试阶段的 classpath,不会添加到编译的 classpath. -
test
正常使用应用程序并不需要的依赖项,仅仅用于测试阶段(包括测试代码的编译和运行)。不会被传递。这个 scope 值通常用于测试的类库,例如 JUnit 和 Mockito。它还用于非测试库,如 Apache Commons IO,如果这些库在单元测试中使用 (src/test/java),而不在模型代码中使用 (src/main/java)。 -
system
与 provided 类似,只是 jar 在本地系统,不在仓库。 -
import
这个作用域仅支持部分中类型为 pom 的依赖项。 它表示该依赖项将被替换为指定 POM 的部分中的有效依赖项列表。 由于它们被替换了,具有 import 作用域的依赖项实际上并不参与限制依赖项的传递性。
下面介绍 scope 如何影响(限制)依赖的传递性。我的项目依赖 A,A 又依赖 B,那么 A 就是我项目的直接依赖,B 就是我项目的间接依赖。以第一行为例,我项目的直接依赖的 A 的 scope 为 compile,间接依赖 B 的 scope 为 compile,可以看到交集为 compile(*),那么传递依赖的 scope 最终为 compile;如果间接依赖 B 的 scope 为 provided,交集为 ”-“,则表示项目不需要该依赖;等等,不一一介绍了。
总之,从图中可以看出,如果间接依赖为 provided 或者 test,则该项目不需要此依赖,如果间接依赖是 compile,则最终间接依赖的 scope 和直接依赖的 scope 相同;如果间接依赖为 runtime,则除了直接依赖 scope 为 compile,间接依赖最终为 runtime,这一点比较特殊为,其他情况,也是和直接依赖相同。
(*)的说明,可以看到直接依赖的 scope 为 compile,其实,在编译阶段是不需要传递依赖,传递依赖的 scope 应为 runtime。但是在推断规则里把传递依赖的 scope 设置为 compile,其主要目的是显示的列出该项目源码程序所使用的所有依赖而已。
另外,直接依赖和间接依赖是相对而言的,例如我项目依赖 A,A 依赖 B,B 依赖 C。那么对于我的项目而言 A 是直接依赖,B 是间接依赖。对于依赖 A(它本身也是一个项目),它的直接依赖是 B,间接依赖是 C。所以,C 在我项目中的是否被依赖,以及依赖的 scope 为何,需要先确定 C 在 A 中最终的依赖,然后确定 C 在我项目中依赖情况。
A(compile) ⇒ B(runtime) ⇒ C(compile)
从前往后决定依赖,具有 “短路效应”,先确定依赖 B 的 scope,根据上表,推断出,依赖 B 的 scope 为 runtime,然后在根据 B 的 Scope 推断 C 的依赖的 scope 为 runtime。
A(compile) ⇒ B(provided) ⇒ C(compile)
依赖 B 的 scope 推断为 “-”,即不需要依赖 B,进而 B 的所有依赖也不需要了。