Maven 教程

Maven 笔记

Maven 依赖管理详解


管理项目依赖是 Maven 的重要核心功能之一,为了能自动化地解析任何一个 Java 构件,Maven 需要将它们进行唯一标识,这就是 Maven 依赖管理的底层基础 —— 坐标

Maven 坐标

Maven 中,任何一个构件必须明确定义自己的坐标,而一组 Maven 坐标是通过一些元素定义的,它们是 groupId、artifactId、version、packaging 和 classifier。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.1.18.RELEASE</version>
    <packaging>jar</packaging>
</dependency>
  • groupId:指项目组织唯一的标识符,常以 Java 的包路径的前缀表示,如上例中,org.springframework 是 Spring 框架下所有项目的前缀,其下包括 spring-web、spring-context 等模块;
  • artifactId:指项目的唯一的标识符,常以项目根目录的名称表示,该元素定义实际项目中的一个 Maven 项目(模块);
  • version:该元素定义项目当前所处的版本,Maven 有一套版本规范,主要是快照(SNAPSHOT)和正式版本(RELEASE);
  • packaging:该元素定义项目的打包方式,是可选项,默认是 jar,即打包后生成 .jar,其它常用的有 war,此时打包后生成 .war 形式的文件;
  • classifier:该元素用来帮助定义构件输出的一些附属构件,附属构件与主构件对应,如常见到的附带的 javadoc 文件和 sources 文件;需要注意的是,classifier 是不能直接定义的

依赖管理

依赖配置

完整的依赖配置,除了坐标的相关的元素之外,还包括如下其它配置项:

<project>
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>...</groupId>
            <artifactId>...</artifactId>
            <version>...</version>
            <type>...</type>
            <scope>...</scope>
            <optional>...</optional>
            <exclusions>
                <exclusion>
                    <groupId>...</groupId>
                    <artifactId>...</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        ...
    <dependencies>
    ...
</project>
  • groupIdartifactIdversion:依赖的基本坐标;
  • type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,其默认值是 jar;
  • scope:依赖的范围;
  • optional:标记依赖是否可选,默认是 false;
  • exclusions:用来排除传递性依赖。

依赖范围

首先需要知道,Maven 在编译项目时使用一套 classpath;依赖范围就是用来控制与三种 classpath(编译 classpath测试 classpath运行 classpath) 的关系。

Maven 有以下几种依赖范围,即 scope 元素对应的可选值:

  • compile:编译依赖范围,默认使用该依赖范围;该依赖范围对于编译、测试和运行都有效;
  • test:测试依赖范围,该依赖范围只对于测试 classpath 有效,典型的例子是 JUnit;
  • runtime:运行时依赖范围,此依赖范围对于测试和运行都有效;
  • provided:已提供依赖范围,此依赖范围对于编译和测试有效,但在运行时无效;典型的例子是 servlet-api,编译和测试时需要,但在运行时由于容器已提供,就不需要重复引入;
  • system:系统依赖范围,使用时,必须依赖 systemPath 元素显性地指定依赖文件的路径,有效范围与 provided 一样;由于此类依赖不是通过 Maven 仓库解析的,往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用;此外,systemPath 元素可以引用环境变量,如 ${java.home} 等。
  • import:导入依赖范围,从 Maven 2.0.9 开始支持,此依赖不会对三种 classpath 产生实际影响。
依赖范围与 classpath 关系
依赖范围(scope) 编译 classpath 测试 classpath 运行 classpath 例子
compile Y Y Y spring-core
test - Y - JUnit
runtime - Y Y JDBC 驱动实现
provided Y Y - servlet api
system Y Y - 本地的,maven 仓库意外以外的

更多信息可以参考 Maven scope 依赖范围作用及使用详解

依赖传递与冲突调解

当依赖的构件也依赖其它构件时,它上游的其它依赖也加入进来,如下:

A -> B -> C

当依赖构件 A 时,由于 A 依赖 B,B 又依赖 C,所以依赖构件 A 会最终也将 B 和 C 加入到依赖里。

Maven 引入的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下,我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但有时候,当传递性依赖造成不同版本的依赖同时引入时,我们就需要清楚地知道该传递性依赖是从哪条依赖路径引入的。

假设项目 A 有如下依赖关系:

A -> B -> C -> X(1.0)
A -> D -> X(2.0)

根据依赖传递性,A 项目在两条路径上依赖两个版本的 X,那应该选择依赖哪一个呢?

Maven 依赖冲突的调解第一原则是路径最近者优先,因此如上示例会选择第二条路径的 X(2.0) 依赖。

依赖调解第一原则不能解决所有冲突问题,如下示例:

A -> B -> Y(1.0)
A -> C -> Y(2.0)

Y(1.0) 和 Y(2.0) 的路径长度一样,在 Maven 2.0.9 版本开始之后,它定义了第二原则是第一声明者优先,即在 pom 中谁先声明谁先被采用。

可选依赖

如果当前的依赖项不想进行传递,可以将上游依赖项的 optional 设置为 true,示例如下:

A -> B
  -> C

假设 A 依赖 B 和 C,如果 B 和 C 的 optional 设置为 true 时,当有项目依赖 A 时,它不会引入 B 或 C 依赖,即 B 和 C 失去传递性。

排除依赖

传递性依赖会给项目隐式地引入很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。

可以使用 exclusions 元素中指定要排除的 maven 构件,每个都放在 exclusion。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.0.9.RELEASE</version>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>