bboyjing's blog

Maven学习笔记六【依赖机制】

本节来学习Maven地依赖机制。

依赖关系管理是Maven的一个核心特性。管理单个项目的依赖关系很容易。管理由数百个模块组成的项目的依赖关系是可能的。Maven在定义、创建和维护具有良好定义地类路径和库版本的可重复构建方面帮助很大。

传递依赖

Maven通过自动包含传递依赖项,来避免查找和指定自己的依赖项所需要的库。

通过从指定的远程存储库中读取依赖项的项目文件,可以简化该特性。通常,这些项目的所有依赖项都在项目中使用,项目从其父项目继承的依赖项或从其依赖项继承的依赖项也是如此。

可以从依赖项收集的几部数量没有限制,只有在发现循环依赖关系时才会出现问题。

通过传递依赖关系,所包含库地图可以快速增长到相当大。由于这个原因,还有一些附加特性限制了所包含的依赖关系:

  • Dependency mediation - 这决定了当依赖项遇到多个版本时,将选择哪个版本。Maven选择“最近的定义”。也就是说,它使用依赖关系树中与项目最接近地依赖关系版本。我们可以通过在项目的POM重显式地声明版本来保证它。请注意,如果两个依赖版本在依赖树中的深度相同,则第一个声明获胜。
    • “最近的定义” 意味着所使用的版本将是依赖关系树中与项目最接近的版本。例如,如果A、B和C地依赖关系定义为A -> B -> C -> D2.0和A -> E -> D1.0。那么,在构建A时将使用D1.0,因为从A到D,第二条路径更短。可以显式地在A中添加对D2.0的依赖,以强制使用D2.0。
  • Dependency management - 这允许项目作者直接指定在传递依赖项中或在没有指定版本的依赖项中药使用的版本。在上面的示例中,依赖项直接添加到A中,即使A没有直接使用它。相反,A可以在dependencyManagement部分中包含D作为依赖项,并直接控制在引用D时将使用哪个版本。
  • Dependency scope - 这允许只包含适合于构建当前阶段的依赖项,下面会详细讲到。
  • Excluded dependencies如果项目X依赖于项目Y,而项目Y依赖于项目Z,项目X的所有者可以使用exclusion标签显式地将项目Z排除。
  • Optional dependencies - 如果项目Y依赖于项目Z,项目Y的作者可以使用optional元素将项目Z标记为可选依赖项。当项目X依赖于项目Y时,X只依赖于Y,而不依赖于Y的可选依赖项Z。(将可选依赖项视为默认排除可能会有助于理解)

虽然可传递依赖项可以帮助我们包含隐含的所需依赖项,但是最好是明确指定自己的源代码中所使用的依赖项目。当依赖的项目改变了它本身的依赖关系时,这个最佳实践证明了其价值。

比如项目A依赖了项目B,并且项目B依赖了项目C。如果直接使用项目C中的组件,而不是在项目A中指定项目C,则当项目B突然更新/删除项目C的依赖时,它可能会导致生成失败。

直接指定依赖关系的另一个原因是,它为我们的项目提供了更好的文档:可以通过阅读项目中的POM文件了解更多信息。

Maven还提供了dependency:analyze插件来分析这些依赖关系:它有助于使这种最佳实践更容易实现。

Dependency Scope

依赖范围用于限制依赖的传递性,并且还用于影响用于各种构建任务的类路径。

有6种scope可用:

  • compile

    如果没有指定的话,这是默认作用域。编译依赖项再项目的所有类路径中都可用。此外,这些依赖关系被传播到依赖的项目。

  • provided

    这非常类似于编译,但是表示希望JDK或容器再运行时提供依赖项。例如,在为Java企业应用构建Web应用程序时,您需要将Servlet API和相关Java EE API的依赖关系设置为provided范围,因为web容器提供了这些类。此作用域仅在编译和测试类路径中可用,并且是不可传递的。

  • runtime

    此范围指示编译时不需要依赖项,而执行时需要。它位于运行时和测试类路径中,而不是编译路径中。

  • test

    此范围表明该依赖关系不是应用程序正常使用所必需的,并且仅在测试编译和执行阶段可用。这个分为时不可传递的。

  • system

    这个作用与provided作用域类似,同之处在于该依赖不从maven仓库中提取,而是从本地文件系统中提取,其会参照systemPath的属性进行提取依赖

  • import

    这个作用域只支持打包类型为pom的<dependencyManagement>标签部分。它指示将依赖项替换为<dependencyManagement>标签中的有效依赖项列表。能解决maven单继承问题,由于它们被替换,具有导入范围的依赖项实际上不参与限制依赖项地传递。

每个作用域(import除外)以不同的方式影响传递依赖关系,如下表所示。如果将依赖项设置为左列中的范围,则该依赖项与顶行作用域之间的可传递依赖项将导致主项目中的依赖项与交叉点处列出的作用域之间存在依赖关系。如果没有列出范围,则表示将忽略依赖项。

compile provided runtime test
compile compile(*) - runtime -
provided provided - provided -
runtime runtime - runtime -
test test - test -

(*)注意:这应该是运行时作用域,因此必须显式列出所有编译依赖项。但是,在这种情况下,所依赖的库从另一个库扩展了一个类,迫使我们在编译时具有可用性。因此,及时编译时依赖项是可传递的,他们仍然是编译范围。

Dependency Management

依赖关系管理部分是一种集中依赖关系信息的机制。当我们有一组继承公共父类的项目时,可以将有依赖关系的所有信息按在公共POM中,使得子项目可以更简单地引用。通过例子可以很好地说明这种机制。考虑到两个pom扩展了相同的父pom:

Project A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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>

Project B:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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就变得简单多了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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。多数情况下,这些依赖关系将引用没有classifier的jar项目,因此type字段默认为jar,classifier默认为null。

依赖关系管理部分的第二个非常重要的用途是控制可传递依赖关系中使用的版本,以这些项目为例:

Project A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<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>

Project B:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<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版本1.0上运行时,无论其pom中指定的版本如何,都将使用a,b,c和d:

  • a和c都被申明为项目的依赖项,由于Dependency mediation的介入,而使用版本1.0.两者都将具有运行时范围,因为它是直接指定的
  • b在B的父项目依赖管理关系中定义,对于传递依赖项,依赖管理优先于依赖中介,所以如果在a或c的pom中引用它,将选择版本1.0。b也有编译范围。
  • 最后,由于d在B的依赖关系管理部分中指定,如果d是a或c的依赖关系(或传递依赖关系),将选择1.0版本–再次因为依赖关系管理优于依赖关系中介,也因为当前pom的声明需要优于其父项目的声明。

导入依赖项

上面的示例描述了如何通过继承来指定托管依赖项。但是,在较大的项目中,可能无法实现此目标,因为项目只能继承单个父项目。为了适应这种情况,项目可以从其它项目导入托管依赖项。这是通过将pom申明为具有import范围的依赖项来实现的。

Project B:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<project>
<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>maven</groupId>
<artifactId>A</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>

假设A是前面例子中定义的pom,最终结果将是相同的。除了d之外,所有A的托管依赖项都将被合并到B中,因为d是在当前pom中定义的。

Project X:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>X</artifactId>
<packaging>pom</packaging>
<name>X</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>b</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

Project Y:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>Y</artifactId>
<packaging>pom</packaging>
<name>Y</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>test</groupId>
<artifactId>a</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>test</groupId>
<artifactId>c</artifactId>
<version>1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

Project Z:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>maven</groupId>
<artifactId>Z</artifactId>
<packaging>pom</packaging>
<name>Z</name>
<version>1.0</version>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>maven</groupId>
<artifactId>X</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>maven</groupId>
<artifactId>Y</artifactId>
<version>1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

在上面的示例中,Z从X和Y导入托管依赖项。但是,X和Y都包含依赖项a。在这里,将使用版本1.1,因为X首先被申明,而且在Z的dependencyManagement中没有申明。

这个过程是递归的。例如,如果X导入了另一个pom,Q。当处理Z时,将显式所有Q的依赖管理项都在X中定义。

通常当用于定义多个项目构建的一部分的相关工件的“库”时,导入最有效。一个项目使用这些库中的一个或多个工件是相当常见的。但是,有时很难使用与库中分发的版本同步的工件来保留项目中的版本。下面的模式说明了如何创建“物料清单”(BOM)以供其他项目使用。

项目的根是BOM pom。它定义了将在库中创建的所有工件的版本。希望使用该库的其他项目应将此pom导入其pom的dependencyManagement部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>bom</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<project1Version>1.0.0</project1Version>
<project2Version>1.0.0</project2Version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>project1</artifactId>
<version>${project1Version}</version>
</dependency>
<dependency>
<groupId>com.test</groupId>
<artifactId>project2</artifactId>
<version>${project1Version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>parent</module>
</modules>
</project>

parent子项目将BOM pom作为其父项目。这是一个正常的多项目pom。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<version>1.0.0</version>
<artifactId>bom</artifactId>
</parent>
<groupId>com.test</groupId>
<artifactId>parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>project1</module>
<module>project2</module>
</modules>
</project>

接下来是实际的项目pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<version>1.0.0</version>
<artifactId>parent</artifactId>
</parent>
<groupId>com.test</groupId>
<artifactId>project1</artifactId>
<version>${project1Version}</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
</dependencies>
</project>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.test</groupId>
<version>1.0.0</version>
<artifactId>parent</artifactId>
</parent>
<groupId>com.test</groupId>
<artifactId>project2</artifactId>
<version>${project2Version}</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</dependency>
</dependencies>
</project>

下面的项目展示了如何在另一个项目中使用这个库,而不必指定依赖项目的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>use</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>project1</artifactId>
</dependency>
<dependency>
<groupId>com.test</groupId>
<artifactId>project2</artifactId>
</dependency>
</dependencies>
</project>

最后,在创建导入依赖项的项目时,请注意以下事项:

  • 不要尝试导入当前pom的子模块中定义的pom。尝试这样做会导致构建失败,因为它无法找到pom
  • 永远不要申明导入pom作为目标pom的父pom。没有办法解决循环,并抛出异常。
  • 当引用其poms具有传递依赖性的工件时,项目需要将这些工件的版本指定为托管依赖项。不这样做会导致构建失败,因为工件可能没有指定版本。

System Dependencies

重要提示:此标记已弃用。

范围系统的依赖关系始终可用,并且不会在存储库中查找。它们通常用于告诉Maven有关JDK或VM提供的依赖关系。因此,系统依赖性对于解决现在由JDK提供的工件的依赖性特别有用,但是可以在之前单独下载。典型示例是JDBC标准扩展或Java身份验证和授权服务(JAAS)。

一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
...
<dependencies>
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
</dependencies>
...
</project>

如果我们的工件由JDK的tools.jar提供,则系统路径将定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
...
<dependencies>
<dependency>
<groupId>sun.jdk</groupId>
<artifactId>tools</artifactId>
<version>1.5.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
...
</project>