七的博客

快速上手Maven(二)-依赖管理

Maven

快速上手Maven(二)-依赖管理

Maven 依赖管理是 Maven 这个软件最重要的一个功能之一,它可以极大地帮助开发人员自动解决项目依赖问题。

核心还是通过 POM 文件中的定义,Maven 通过解析这些定义中的依赖关系,从而降低了开发人员的工作量。下面的几点内容都是针对 pom.xml 中的依赖部分进行讲解。

熟练了解并掌握下面的几个概念,是掌握 Maven 基本用法很核心的一个环节。

1. 坐标的基本概念

在 Maven 中 , 每个项目、库都有一个唯一的坐标 ( GAV ) , 它由三部分组成:

  • groupId : 项目组 ID , 通常是项目的包名。 通常会使用公司或组织的反向域名 , 比如你公司的域名是 abc.com ,那么 groupId 通常就是取为 com.abc。不过由于公司底下的业务线可能比较多,这时候可能会在 groupId 上加业务线表示,比如 com.abc.pay 表示这是支付相关的产品线。 这样取名有助于保证 groupId 的全局唯一性。

  • artifactId : 通常就是项目名称或者模块名称,如果你的项目是个单模块的,那么就直接具体到项目名称。 如果你的项目是多模块,那么就会是模块名称。比如 pay-api、pay-util、pay-common 等等。

  • version : 项目的版本号。在业内大部分的版本号都遵循 x.y.z 形式,即主版本号.次版本号.修订号。 你可以观察平时一些 APP 上更新的版本提示,基本都是符合这个原则。版本号遵循的原则是只升不降,

比如拿一个 Spring context 的依赖坐标 org.springframework:spring-context:4.3.19.RELEASE 来拆解下:

  • groupId 是 org.springframework
  • artifactId 是 spring-context
  • version 是 4.3.19.RELEASE

有了这个坐标 , Maven 就可以从仓库中找到并下载对应的依赖。要在项目中使用这个依赖的话 , 直接在项目的 pom.xml 文件中声明这个依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>4.3.19.RELEASE</version>
    </dependency>
</dependencies>

这样 Maven 在构建项目时 , 就会 自动下载这个依赖以及这个依赖相关的依赖 , 并将其加入项目的 classpath 中。

比如上面的 spring-context:4.3.19.RELEASE 本身还会依赖其他的 JAR 包:

spring-context 依赖列表

从上面可以看得出来,引入 spring-context 的时候,也间接的引入了其他几个 spring 的依赖。这一点有很多的好处,同时在项目引入的开源库多之后,也容易造成很多麻烦。

2. 依赖范围

依赖范围可以理解为一个 JAR 包在项目中的可见度。 这个范围配置会决定这个 JAR 包在编译、测试、运行这几个阶段是否可以使用,同时会不会包含在最终打包出来的 JAR包或者 WAR 包中。

先看下在哪里配置,拿 HelloWorld 例子中的依赖为例:

  
<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>3.8.1</version>
        <scope>test</scope>  // 注意这里
    </dependency>
</dependencies>

上面的 junit 是 Java 中最常用的一个测试工具,主要是用于进行单元测试。 这里声明的 scope 为 test ,说明只会在测试阶段生效。

在 Maven 中主要有下面几种依赖的范围:

2.1 compile

这个范围是最常见,也是默认的一个依赖范围。 你引入一个依赖的时候,默认就是 compile 。如上面的 spring-context :

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.19.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>4.3.19.RELEASE</version>
    <scope>compile</scope>
</dependency>

上面这两种写法是等价,因为没有指定范围,默认就是 compile。

这种范围的依赖可以在所有阶段使用,包括编译阶段、测试阶段、运行阶段。 在运行阶段可用,所以该依赖最终会被包含在最终打包的 JAR 或者 WAR 中。

同时依赖也会被传递,这一点在下面的小节会讲。

2.2 provided

provided 范围跟 compile 是类似的,区别在于运行阶段是不可用的。 也就是说,最终打包的 JAR 里面是没有这个依赖的,通常这个依赖的 JAR 包会由运行的容器等提供。

一个典型的例子就是是 servlet-api 。开发一个 Web 应用时 , 需要 servlet-api 来编译和测试代码 , 但因为 servlet-api 将由Web容器提供, 所以最终打包的时候不应该包含在 JAR 中。 这个时候就可以将范围指定为 provided 即可。

 
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

2.3 runtime

runtime 范围的依赖在编译时是不需要的 , 但在测试和运行时是需要。

比较典型的一个例子就是 JDBC 驱动,代码在编译时只需要依赖 JDBC 的 API 。运行时一般通过反射去读取对应驱动的 JAR 包,这时候是需要 JAR 包存在的。

<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.2.5</version>
     <scope>runtime</scope>
</dependency>

2.4 test

test 范围的依赖只在测试编译和测试运行时可用 , 在编译环节和运行环节时都不可用。

通常就是一些测试框架 , 如 JUnit 或 Mockito 应该声明成这个范围,避免这些依赖传递给其他项目中,同时最终打包的时候也不会包含这些依赖。

 
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>3.8.1</version>
    <scope>test</scope>
</dependency>

2.5 system

system 就是当你显式指定一个本地 jar 包路径的时候,就需要指定对应 jar 包的路径。 这个 jar 会直接从本地仓库中去取,这就导致了一个问题,如果项目换一台电脑很有可能会跑不起来。

所以一般情况下是不推荐使用,在某些情况下 jar 无法从中央仓库或者私服上,就只能使用这种依赖范围。

<dependency>
    <groupId>com.suny</groupId>
    <artifactId>special-lib</artifactId>
    <version>1.0.0</version>
    <scope>system</scope>
    <systemPath>/path/lib.jar</systemPath>
</dependency>

2.6 import

这是一个比较特殊的范围,只能在 <dependencyManagement> 标签中进行使用。它表示从其他 pom 文件中的依赖项。

import 范围常用于多模块项目 , 其中有一个父 POM 文件定义了所有的依赖版本 , 然后子模块可以从父 POM文 件导入这些版本信息。

这样可以确保所有子模块使用相同的依赖版本,从而避免版本冲突。在 Spring boot 中的依赖,就是使用了这种写法:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

3. 依赖传递

Maven 的依赖传递 ( Dependency Transitivity ) 是一种机制 , 通过这种机制 , 当你的项目依赖于某个库时 , 你也会自动依赖于这个库所依赖的其他库。

这种传递性依赖可以递归,从而形成一个依赖树。在第一个小节中,有提到过这个依赖传递。引用了一个 spring-context ,但是最终会自动包含 spring-aop、spring-core、spring-beans 等几个依赖。

spring-context 依赖列表

依赖传递特性可以在很大程度上简化依赖管理,你只需要引入对应库的依赖即可,无需关心这个库自身的依赖。

这种间接依赖也会导致几个常见的问题:

  • 版本冲突 : 在一些大的项目中,经常容易出现这个问题。多个库依赖于同一个库的不同版本 , 就容易导致版本冲突。

  • 不必要的依赖 : 有时只是想引用一个小小的依赖,但是由于传递依赖引入一些你的项目实际上并不需要的库 , 这可能会增加项目的大小和复杂性。业务代码总共没几行,打个包出来动不动就几十兆。

4. 依赖冲突解决

上面小节提到了依赖冲突的问题,假设这种情况:

假设项目有两个模块 A 和 B , 模块 A 依赖 C 的 1.0 版 , 模块 B 依赖 C 的 2.0 版 , 那么项目应该使用 C 的哪个版本?

Project
  ├─ Module A
  │    └─ Library C (version 1.0)
  └─ Module B
       └─ Library C (version 2.0)

在这种情况下, Maven 使用 “最近优先” 的原则来解决依赖冲突。

如果有多个版本的依赖 , Maven 会选择最近的版本。所谓最近 , 指的是指在依赖树中距离项目最近。

在上面的例子中 , 模块 A 和 B 都直接被项目依赖 , 所以它们的距离相同 , Maven 会选择版本较高的 2.0。

如果想手动解决依赖冲突,比较常用的有以下几种方式。

4.1 约束项目依赖版本

可以在 <dependencyManagement> 中强制指定版本 , 在这个元素中声明的依赖不会实际引入 , 而是用于约束项目的依赖版本。举个例子:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.suny.maven</groupId>
            <artifactId>Library-C</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

这样 , 无论 A 模块和 B 模块依赖的是 C 的哪个版本 , 在项目中 , C 的版本都会被锁定为 1.0。

4.2 排除依赖

还有些情况下, 可能需要排除某些传递性依赖。比如 , A 依赖 B , B 依赖 C , 但是项目中不想使用 C , 这时可以在依赖 A 时排除 C:

<dependency>
    <groupId>com.suny.maven</groupId>
    <artifactId>A</artifactId>
    <version>1.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.suny.maven</groupId>
            <artifactId>Library-C</artifactId>
        </exclusion>
    </exclusions>
</dependency>

5. 参考链接