Maven 依赖配置

Maven 依赖配置的基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<project>
......
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>..</optional>
<exclusions>
<exclusion>
...
</exclusion>
...
</exclusions>
</dependency>
...
</dependencies>
......
</project>

通常,一个项目都会用到一个或者多个项目依赖。因此,在 pom.xml 的根元素 project 下的 dependencies 可以包含一到多个 dependency 元素,以声明一或者多个项目依赖。

一个 dependency 元素可以包含 groupId、artifactId、version、type、classifier、scope、systemPath、optional、exclusions 等元素。其中 groupId、artifactId、version 对每一个 dependency 元素配置是必须的。optional 用于标记依赖是否可选。另外,systemPath 仅当 scope 为 system 使用,官网上明确不赞成使用。

依赖项目坐标:groupId、artifactId 和 version

groupId、artifactId、version 对每一个 dependency 元素来说是必须的。它们是依赖的Maven坐标,对于任何一个 Maven 项目而言,基本坐标是最重要的,Maven 会根据坐标从资源库中找到需要的依赖。

version 配置

version 元素定义了依赖项目的版本需求,用于计算有效的依赖版本号。version 允许使用如下语法格式:

  • 1.0: 对1.0版本的非硬性需求(建议版本)
  • [1.0]: 硬性需求1.0版本
  • (,1.0]: x <= 1.0
  • [1.2,1.3]: 1.2 <= x <= 1.3
  • [1.0,2.0): 1.0 <= x < 2.0
  • [1.5,): x >= 1.5
  • (,1.0],[1.2,): x <= 1.0 or x >= 1.2;多个设置用逗号分隔
  • (,1.1),(1.1,): 排除1.1版本

例如:配置 junit 依赖如下,Maven 将计算使用 junit-4.3.jar 作为 test scope 依赖包。

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>[4.1,4.3]</version>
<scope>test</scope>
</dependency>

版本规则接受使用“.”或“-”来组合“版本数字(number tokens 数字标记)”和“版本限定词(non-number tokens 非数字标记)”形成的字符串系列。该系列在被计算时将会从后开始将(0, “”, “final”, “ga”)去掉,如此:

  • “1.0.0” -> 1
  • “1.ga” -> 1
  • “1.final” -> 1
  • “1.0” -> 1
  • “1.” -> 1
  • “1-“ -> 1
  • “1.0.0-foo.0.0” -> 1-foo
  • “1.0.0-0.0.0” -> 1

版本序列比较规则:

  • 数字标记部分采用自然序(natural order)。
  • 非数字标记(版本限定词)部分采用字典序(alphabetical order),除了业界习惯性版本规则:

“alpha” < “beta” < “milestone” < “rc” = “cr” < “snapshot” < “” = “final” = “ga” < “sp” “.qualifier” < “-qualifier” < “-number” < “.number”

例如:

  • “1” < “1.1”
  • “1-snapshot” < “1” < “1-sp”
  • “1-foo2” < “1-foo10”
  • “1.foo” < “1-foo” < “1-1” < “1.1”
  • “1.ga” = “1-ga” = “1-0” = “1.0” = “1”
  • “1-sp” > “1-ga”
  • “1-sp.1” > “1-ga.1”
  • “1-sp-1” < “1-ga-1” = “1-1”
  • “1-a1” = “1-alpha-1”

classifier 附属构件配置

classifier 通常用于区分从同一POM构建的具有不同内容的构件(artifact)。它是可选的,它可以是任意的字符串,附加在版本号之后。需要紧跟 version 元素进行书写。其主要有两个用途:

  • 指定依赖项目组成部分,如源代码、javadoc、类文件等

如需要 junit 的 javadoc 包 junit-4.3-javadoc.jar,则配置 dependency 如下:

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.3</version>
<classifier>javadoc</classifier>
</dependency>

若需要源码包 junit-4.3-sources.jar,则可配置 如下:

1
2
3
4
5
6
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.3</version>
<classifier>sources</classifier>
</dependency>
  • 指定依赖项目的JDK版本

如项目依赖 json-lib-2.2.2-jdk13.jar,则配置 dependency 如下:

1
2
3
4
5
6
<dependency>  
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.2</version>
<classifier>jdk13</classifier>
</dependency>

若项目依赖 json-lib-2.2.2-jdk15.jar,则可配置如下:

1
2
3
4
5
6
<dependency>  
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.2.2</version>
<classifier>jdk15</classifier>
</dependency>

依赖类型:type

type 元素用于声明依赖的类型,对应于项目坐标定义的 packaging(打包方式)。大多数情况下,该元素不必特别声明,其缺省值为 jar。

依赖范围:scope

Maven 在编译项目主代码(即编译 compile 范围)时需要使用一套 classpath,在编译运行测试用例(即测试 test 范围)时会需要使用另外一套 classpath。最后,实际发布运行时可能又会使用一套 classpath。比如一个 J2EE 项目,在编译主代码时通常需要依赖 servlet-api 包,在编译运行测试代码时需要依赖 junit 包,而在部署到服务器时则无需这两个包。

依赖范围就是用来控制依赖与这三种 classpath(编译 classpath、测试 classpath、运行 classpath)的关系,Maven 有以下几种依赖范围:

  • compile:编译依赖范围。如果没有指定 scope,就会默认使用该依赖范围。compile 依赖范围,对于编译、测试、运行三种 classpath 都有效。
  • test:测试依赖范围,仅对于测试 classmate 有效。典型的例子是 JUnit,只有在编译和运行测试的时候才需要。
  • provided:已提供依赖范围。该依赖范围,对于编译和测试 classpath 都有效,但对运行 classpath 无效。典型的例子是 servlet-api,编译和测试 J2EE 项目时都需要该依赖,但在部署运行项目时,由于容器已经提供,故不需要重复引入。
  • runtime:运行时依赖范围。该依赖范围,对于测试和运行 classpath 有效,但在编译主代码时无效。典型的例子是 JDBC 驱动实现,项目主代码的编译只需要 JDK 提供的 JDBC 接口,只有在测试和项目运行时才真正需要。
  • system:系统依赖范围。系统依赖范围和三种 classpath 的关系,和 provided 依赖范围完全一致。但是由于此类依赖不是通过 Maven 仓库解析,而是往往与本机系统绑定,在使用时必须通过 systemPath 元素显示地指定依赖文件的路径。可能造成构建的不可移植,因此应该谨慎使用。
  • import:导入依赖范围。该依赖范围为 Maven 2.0.9 之后新增,它只使用在 中,表示从其它的 pom 中导入 dependency 的配置。

    systemPath 元素可以引用环境变量,如:

1
2
3
4
5
6
7
<dependency>  
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
依赖范围与 classpath 的关系
依赖范围 对编译classpath有效 对测试classpath有效 对运行时classpath有效 典型例子
compile Y Y Y json
test - Y - junit
provided Y Y - servlet-api
runtime - Y Y JDBC 驱动实现
system Y Y - 本地的,Maven仓库之外的类库文件

传递性依赖机制

何为传递性依赖

传递性依赖,举例来说就是“项目A”有一个 compile 范围的“项目B”依赖,同时“项目B”又有一个 compile 范围的“项目C”依赖,那么“项目C”就会成为“项目A”的 compile 范围依赖,“项目C”就是“项目A”的一个传递性依赖。

有了传递性依赖机制,Maven 会解析各个直接依赖的 POM,将那些必要的间接依赖以传递依赖的形式引入到项目中。一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。

注意:
可选依赖,即 optional 为 true 的依赖,依赖将不会得以传递。原则上,是不应该使用可选依赖的。因为使用可选依赖的原因通常是某一项目实现了多个特性,且这些特性分别被多个项目所依赖,这违背了面向对象设计的“单一职责性原则”。

传递性依赖范围

依赖范围不仅可以控制依赖与三种 classmate 的关系,还对传递性依赖产生影响。假设 A 依赖于 B,B 依赖于 C,我们说 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖。那么第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围。

传递性依赖范围规则
第一直接依赖范围 \ 第二直接依赖范围 compile test provided runtime
compile compile - - runtime
test test - - test
provided provided - provided provided
runtime runtime - - runtime

规律:

  • 当第二直接依赖的范围是 compile 时,传递性依赖的范围与第一直接依赖的范围一致;
  • 当第二直接依赖的范围是 test 时,依赖不会得传递;
  • 当第二直接依赖的范围是 provided 时,只传递第一直接依赖范围也是 provided 的依赖,且传递性依赖的范围同样也为 provided;
  • 当第二直接依赖的范围是 runtime 的时候,除 compile 对应性依赖范围为 runtime 外,其余皆与第一直接依赖的范围一致。

依赖调解原则

Maven 为依赖冲突定义了两条调解原则:

  • 第一原则:同一依赖构件,不同版本,路径最近者优先。

    例如,项目 A 对 X 有这样的两条依赖路径:
    A -> B -> C -> X(1.0)
    A -> C -> X(2.0)

    依据第一原则,X(2.0)将会被解析使用。

  • 第二原则:同一依赖构件,不同版本,若路径相同,则第一声明者优先。

    同一依赖构件,不同版本,路径也相同的情况在 Maven 2.0.8 及之前的版本中是不确定的,但从 Maven 2.0.9 开始,为了尽可能避免构建的不确定性,Maven 定义了依赖调解的第二原则。

    例如,同样有两条依赖路径:
    A -> B -> Y(1.0)
    A -> C -> Y(2.0)

    依据第二原则,Y(1.0)将会被解析使用。

最佳实践

依赖排除:exclusions

传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但有些时候这种特性也会带来问题。比如,传递性依赖的项目版本过旧或不稳定,或者涉及版权问题。那么就需要排除传递性依赖,而用替代的直接依赖。这是就需要用到 exclusions 元素。

exclusions 元素可以包含一个或者多个 exclusion 子元素,可以排除一个或者多个传递性依赖。需要注意的是,声明 exclusion 的时候只需要 groupId 和 artifactId,而不需要 version 元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>com.xxx.xxx</groupId>
<artifactId>dp01</artifactId>
<version>1.0</version>

<exclusions>
<exclusion>
<groupId>com.xxx.xxx</groupId>
<artifactId>dp02</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>com.xxx.xxx</groupId>
<artifactId>dp02-replace</artifactId>
<version>1.0</version>
</dependency>

依赖归类

对于来自同一项目的不同模块,如 org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6 和org.springframework:spring-context-support:2.5.6,如果需要对这些模块同时进行依赖,通常所有这些依赖的版本都是相同的。而且可以预见,如果将来需要升级,这些依赖的版本会一起升级。这时,就需要考虑进行依赖归类,在 POM 中使用 properties 元素引入版本变量。

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
<project>
......

<properties>
<spring.version>2.5.6</spring.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
......
</project>

依赖优化

Maven 提供了一些命令以帮助我们优级化依赖配置。

  • 查看项目依赖
1
mvn dependency:list
  • 查看项目依赖树
1
mvn dependency:tree
  • 分析项目依赖
1
mvn dependency:analyze

注意:
dependency:analyze 只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它无能为力。

参考资源

显示 Gitment 评论