依赖关系管理是Maven的核心功能。管理单个项目的依赖关系很容易。管理由数百个模块组成的多模块项目和应用程序的依赖关系是可能的。Maven在定义、创建和维护具有良好定义的类路径和库版本的可复制构建方面有很大帮助。
一、传递依赖
Maven通过自动包含可传递的依赖关系,避免了发现和指定您自己的依赖关系所需的库的需要。
通过从指定的远程存储库中读取依赖项的项目文件,可以实现此功能。通常,这些项目的所有依赖项都会在项目中使用,项目从其父项或依赖项继承的任何依赖项也是如此,依此类推。
可以从中收集依赖关系的级别数量没有限制。只有当发现循环依赖关系时,才会出现问题。
有了可传递的依赖关系,包含库的图可以很快变大。因此,还有一些附加功能限制了所包含的依赖项:
- 依赖项中介 - 这决定了在遇到多个版本作为依赖项时将选择哪个版本的项目。Maven选择了"最接近的定义"。也就是说,它使用依赖项树中与项目最近的依赖项的版本。您始终可以通过在项目的 POM 中显式声明版本来保证版本。请注意,如果两个依赖项版本在依赖项树中的深度相同,则第一个声明优先。
-
"最接近的定义"意味着使用的版本将是依赖项树中与您的项目最接近的版本。考虑以下依赖树:
A
├── B
│ └── C
│ └── D 2.0
└── E
└── D 1.0
-
在文本中,A、B和C的依赖项定义为A->B->C->D2.0和A->E->D1.0,然后在构建A时将使用D1.0,因为从A到D通过E的路径更短。您可以在a中显式地向D2.0添加依赖项,以强制使用D2.0,如下所示:
A
├── B
│ └── C
│ └── D 2.0
├── E
│ └── D 1.0
│
└── D 2.0
- 依赖项管理-这允许项目作者在传递依赖项或未指定版本的依赖项中遇到工件时,直接指定要使用的工件版本。在上一节的例子中,一个依赖项被直接添加到a中,尽管a没有直接使用它。相反,a可以将D作为依赖项包含在其dependencyManagement部分中,并直接控制在何时或是否引用它时使用D的哪个版本。
- 依赖关系范围-这允许您只包括适用于构建的当前阶段的依赖关系。下面将对此进行更详细的描述。
- 排除的依赖项-如果项目X依赖于项目Y,而项目Y依赖于项目Z,则项目X的所有者可以使用"排除"元素将项目Z明确排除为依赖项。
- 可选依赖项-如果项目Y依赖于项目Z,则项目Y的所有者可以使用"可选"元素将项目Z标记为可选依赖项。当项目X依赖于项目Y时,X将仅依赖于Y,而不依赖于Y的可选依赖项Z。然后,项目X的所有者可以根据自己的选择显式添加对Z的依赖项。(将可选依赖项视为"默认排除"可能会有所帮助)
尽管可传递依赖项可以隐式地包括所需的依赖项,但显式指定源代码直接使用的依赖项是一种很好的做法。这种最佳实践证明了它的价值,尤其是当项目的依赖关系改变了它们的依赖关系时。
例如,假设您的项目A指定了对另一个项目B的依赖项,而项目B指定了对项目C的依赖项。如果您直接在项目C中使用组件,而您没有在项目A中指定项目C,则当项目B突然更新/删除其对项目C的依赖时,可能会导致生成失败。
直接指定依赖项的另一个原因是它为您的项目提供了更好的文档:只需读取项目中的POM文件,或者执行mvn-dependency:tree,就可以了解更多信息。
Maven还提供了dependency:analyze插件分析依赖关系的目标:这有助于使这种最佳实践更容易实现。
二、依赖范围
依赖关系范围用于限制依赖关系的传递性,并确定依赖关系何时包含在类路径中。
共有6个作用域:
- compile
这是默认范围,如果未指定,则使用。编译依赖项在项目的所有类路径中都可用。此外,这些依赖项将传播到依赖项目。
- provided
这很像编译,但表示您希望JDK或容器在运行时提供依赖关系。例如,当为Java Enterprise Edition构建web应用程序时,您需要将对Servlet API和相关Java EE API的依赖设置为所提供的范围,因为web容器提供了这些类。具有此作用域的依赖项被添加到用于编译和测试的类路径中,但不添加到运行时类路径中。它不可传递。
- runtime
此范围表示编译不需要依赖项,而是执行依赖项。Maven在运行时和测试类路径中包含一个具有此作用域的依赖项,但不包括编译类路径。
- test
此范围表示应用程序的正常使用不需要依赖项,仅适用于测试编译和执行阶段。此范围不可传递。通常,这个范围用于JUnit和Mockito等测试库。它也用于非测试库,如Apache Commons IO,如果这些库用于单元测试(src/test/java),但不用于模型代码(src/main/java)。
- system
这个作用域与提供的作用域类似,只是您必须提供显式包含它的JAR。工件总是可用的,并且不会在存储库中查找。
- import
只有<dependencyManagement>部分中pom类型的依赖项才支持此作用域。它指示依赖项将被指定POM的<dependencyManagement>部分中的有效依赖项列表所取代。由于它们被替换,具有导入范围的依赖项实际上并没有参与限制依赖项的传递性。
每个作用域(导入除外)都以不同的方式影响可传递的依赖项,如下表所示。如果将依赖项设置为左列中的作用域,则该依赖项与顶行中作用域的可传递依赖项将导致主项目中的依赖项与交叉点处列出的作用域。如果没有列出作用域,则表示省略了依赖项。
|----------|-------------|----------|----------|------|
| | compile | provided | runtime | test |
| compile | compile(*) | - | runtime | - |
| provided | provided | - | provided | - |
| runtime | runtime | - | runtime | - |
| test | test | - | test | - |
(*)注意:这应该是运行时范围,因此必须显式列出所有编译依赖项。但是,如果您所依赖的库从另一个库扩展了一个类,则两者在编译时都必须可用。由于这个原因,编译时依赖关系仍然是编译范围,即使它们是可传递的。
三、依赖关系管理
依赖关系管理部分是一种用于集中依赖关系信息的机制。当您有一组从公共父级继承的项目时,可以将有关依赖项的所有信息放在公共 POM 中,并对子 POM 中的项目进行更简单的引用。通过一些例子可以最好地说明这种机制。给定扩展同一父级的这两个 POM:
项目A:
<project>
...
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
项目B:
<project>
...
<dependencies>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
这两个示例 POM 共享一个共同的依赖项,并且每个都有一个非平凡的依赖项。此信息可以像这样放入父 POM 中:
<project>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>group-c</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>war</type>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<version>1.0</version>
<type>bar</type>
<scope>runtime</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
然后,两个子 POM 变得更加简单:
<project>
...
<dependencies>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-a</artifactId>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>bar</type>
</dependency>
</dependencies>
</project>
<project>
...
<dependencies>
<dependency>
<groupId>group-c</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>war</type>
</dependency>
<dependency>
<groupId>group-a</groupId>
<artifactId>artifact-b</artifactId>
<!-- This is not a jar dependency, so we must specify type. -->
<type>bar</type>
</dependency>
</dependencies>
</project>
注意:在其中两个依赖引用中,我们必须指定<type/>元素。这是因为用于将依赖引用与dependencyManagement节匹配的最小信息集实际上是{groupId,artifactId,type,classifier}。在许多情况下,这些依赖关系将引用没有分类器的jar工件。这允许我们将标识集简写为{groupId,artifactId},因为类型字段的默认值是jar,而默认分类器是null。
依赖关系管理部分的第二个也是非常重要的用途是控制可传递依赖关系中使用的工件的版本。以这些项目为例:
项目A:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>A</artifactId>
<packaging>pom</packaging>
<name>A</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
项目B:
<project>
<parent>
<artifactId>A</artifactId>
<groupId>maven</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>B</artifactId>
<packaging>pom</packaging>
<name>B</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>d</artifactId>
<version>1.0</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
当maven在项目B上运行时,无论POM中指定的版本是什么,都将使用工件a、B、c和d的1.0版本。
a和c都被声明为项目的依赖项,因此由于依赖项中介,使用1.0版本。两者都有运行时作用域,因为它是直接指定的。
b是在b的父级依赖关系管理部分中定义的,由于对于可传递依赖关系,依赖关系管理优先于依赖关系中介,因此如果在a或c的POM中引用它,将选择1.0版本。b也将具有编译范围。
最后,由于d是在B的依赖关系管理部分中指定的,如果d是a或c的依赖关系(或传递依赖关系),则将选择1.0版本,这也是因为依赖关系管理优先于依赖关系中介,也因为当前POM的声明优先于其父声明。
有关依赖项管理标记的引用信息可从项目描述符引用中获得。