Maven 依赖

依赖范围

Maven 在编译项目主代码的时候需要使用一套 classpath。其次,在编译和执行测试的时候会使用另外一套 classpath。最后,实际运行 Maven 项目的时候,又会使用一套 classpath(这里的 classpath 指的是 Java 本身的 classpath,和 Maven、IDE 无关,平时新建 Maven 项目看到的 classpath 是 Maven 约定的,IDE 遵循 Maven 的约定生成的,也可以自己定义 classpath)

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

  • compile:编译依赖范围。如果没有指定,默认使用该依赖范围。使用此依赖范围时,对于编译、测试、运行都有效。例如:spring-core,编译、测试、运行时都需要使用该依赖

  • test:测试依赖范围。只对测试 classpath 有效,在编译主代码或者运行项目时无法使用此类依赖。例如:JUnit,它只在编译测试代码以及运行测试的时候才需要

  • provided:已提供依赖范围。对于编译和测试时有效,但在运行时无效。例如:servlet-api,编译和测试项目的时候需要该依赖,但运行时,由于容器已经提供,就不需要 Maven 重复引入了

  • runtime:运行时依赖范围。对于测试和运行 classpath 有效,但在编译主代码时无效。例如:JDBC 驱动实现,编译时只需要 JDK 提供的 JDBC 接口,只有在执行测试和运行时才需要实现上述接口的具体 JDBC 驱动(因为反射)

  • system:系统依赖范围。同 provided。但是,使用 system 范围的依赖时必须通过 systemPath 元素显式地指定依赖文件的路径。由于此类以来不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植。主要用于依赖本地的、且 Maven 仓库之外的类库文件。systemPath 元素可以引用环境变量,如:

<dependency>
  <groupId>javax.sql</groupId>
  <artifactId>jdbc-stdext</artifactId>
  <version>2.5</version>
  <scope>system</scope>
  <systemPath>${JAVA_HOME}/lib/test.jar<systemPath>
</dependency>
  • import:引入依赖范围,该依赖范围不会对三种 classpath 产生实际的影响,详见 dependencyManagement

上述除 import 以外的各种依赖范围与三种 classpath 的关系如下:

依赖范围 对于编译 classpath 有效 对于测试 classpath 有效 对于运行时 classpath 有效 例子
compile Y Y Y spring-core
test —— Y —— JUnit
provided Y Y —— servlet-api
runtime —— Y Y JDBC 驱动实现
system Y Y —— 本地的,Maven 仓库之外的类库文件

传递依赖和依赖范围

依赖一个 a.jar 时,如果 a.jar 依赖 b.jar,b.jar 会被 Maven 自动加载进来

假设 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 ->B ->C ->X(1.0),同时 A ->D ->X(2.0)。X 是 A 的传递性依赖,但是两条依赖路径上有两个版本的 X。因为 X(2.0) 路径更短,所以 2.0 版本的 X 会被解析使用

  • 原则二:第一声明者优先。在依赖长度相等情况下,解析在 pom 中依赖声明中顺序靠前的。例如:A ->B ->X(1.0) 同时 A ->D ->X(2.0)。如果 B 在 D 之前声明,那么 X(1.0) 会被解析

除以上两种原则外,还可以手动排除,例如:A ->B ->X(1.0) 同时 A ->X(2.0)。如果项目 A 希望加载 X(2.0) 可通过 <exclusion> 元素来显式排除

可选依赖

假设:A ->B、B ->X(optional)、B ->Y(optional)。根据传递性依赖的定义,如果所有这三个依赖的范围都是 compile,那么 X、Y 就是 A 的 compile 范围传递性依赖。然而,由于这里 X、Y 是可选依赖,依赖讲不会传递。也就是说,X、Y 不会对 A 有任何影响

为什么要使用可选依赖这一特性呢?可能项目 B 实现了两个特性,其中特性一依赖于 X,特性二依赖于 Y,而且这两个特性是互斥的,用户不可能同时使用两个特性。比如:B 是一个持久层隔离工具包,它同时支持 MySQL 和 PostgreSQL,A 项目依赖 B,在构建 B 时需要这两种数据库的驱动程序,但在使用的时候只会依赖一种数据库

B 项目的依赖声明如下:

<project>
    <modelVersion>1.0.0</modelVersion>
    <groupId>com.xxx.xx</groupId>
    <artifactId>project-b</artifactId>
    <version>2.5</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.10</version>
            <optional>true</optional>
        </dependency>
        <dependency> 
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>8.4-701.jdbc3</version>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

使用 <optional> 元素表示这两个为可选依赖,这时依赖不会传递到 A 项目,当 A 项目需要使用基于 MySQL 数据库时,需要显式声明对 MySQL 的依赖:

<project>
    <modelVersion>1.0.0</modelVersion>
    <groupId>com.xxx.xx</groupId>
    <artifactId>project-a</artifactId>
    <version>2.5</version>
    <dependencies>
        <dependency>
            <groupId>com.xxx.xx</groupId>
            <artifactId>project-b</artifactId>
            <version>2.5</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.10</version>
        </dependency>
    </dependencies>
</project>

另外:在理想的情况下是不应该使用可选依赖的,因为在面向对象设计中,有一个单一职责原则,即一个 jar 的职责应该只有一个。所以在上面的例子中,最好是为 MySQL 和 PostgreSQL 分别创建一个 Maven 项目,基于同样的 groupId 分配不同的 artifactId,如 com.xxx.xx:project-b-mysql 和 com.xxx.xx:project-b-postgresql。然后再各自的 pom 中声明对应的 JDBC 驱动依赖,用户则根据需要选择使用 project-b-mysql 或 project-b-postgresql。由于传递性依赖的作用,就不用再声明 JDBC 驱动依赖了

排除依赖

传递性依赖会给项目隐式地引入很多依赖,但这种特性也会带来问题。例如,当前项目有个第三方依赖,而这个第三方依赖由于某些原因依赖了另一个类库的 SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前的项目。这时就需要排除该 SNAPSHOT,并在当前项目中声明该类库的某个正式发布的版本

<project>
    <modelVersion>1.0.0</modelVersion>
    <groupId>com.xxx.xx</groupId>
    <artifactId>project-a</artifactId>
    <version>1.0.0</version>
    <dependencies>
        <dependency>
            <groupId>com.xxx.xx</groupId>
            <artifactId>project-b</artifactId>
            <version>1.0.0</version>
            <exclusions>
                <groupId>com.xxx.xx</groupId>
                <artifactId>project-c</artifactId>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.xxx.xx</groupId>
            <artifactId>project-c</artifactId>
            <version>1.1.1</version>
        </dependency>
    </dependencies>
</project>

上述代码中,项目 A 依赖于 B,但是由于某些原因,不想引入传递性依赖 C,而是自己显示地声明对于项目 C 1.1.1 版本的依赖。代码中使用 exclusions 元素声明排除依赖,exclusions 可以包含一个或多个 exclusion 子元素,因此可以排除一个或多个传递性依赖

归类依赖

有些依赖来自同一项目的不同模块,比如 org.springframework:spring-core:2.5.6、org.springframework:spring-beans:2.5.6、org.springframework:spring-context:2.5.6,所有这些依赖的版本都是相同的,如果将来需要升级 Spring Framework,这些依赖的版本会一起升级

就像用常量 PI 定义圆周率一样,Maven 也可以在一个唯一的地方定义版本,并且在 dependency 声明中引用这一版本。这样在升级 Spring Framework 时就只需修改一处:

<project>
    <modelVersion>1.0.0</modelVersion>
    <groupId>com.xxx.xx</groupId>
    <artifactId>project-a</artifactId>
    <version>1.0.0</version>

    <properties>
        <springframework.version>2.5.6</springframework.version>
    </properties>

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

依赖优化

代码需要不断重构才能达到最优,依赖管理也是一样,需要不断的进行去除多余依赖,以及显式的声明某些必要的依赖

Maven 会自动解析所有项目的直接依赖和传递依赖,并根据规则判断每个依赖的范围,对于一些依赖冲突也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在。在这些工作之后得到这个项目的完整的已解析依赖

可通过运行以下命令查看当前项目的已解析依赖:

mvn dependency:list
已解析依赖查看

上图展示了当前项目中所有已解析的依赖,同时每个依赖的范围也得以明确标示

如果将直接在 pom 中声明的依赖定义为第一层依赖,这些顶层依赖的依赖定义为第二层依赖,则以此类推可以形成一个完整的依赖树

可运行以下明细查看当前项目的依赖树

mvn dependency:tree
依赖树

可运行以下命令对当前项目依赖进行简单分析

mvn dependency:analyze
依赖分析

上图中 Used undeclared dependencies,表示项目中使用到的,但是没有显式声明的依赖。这种依赖意味着潜在的风险,当前项目直接在使用它们,例如有很多相关的 Java import 声明,而这种依赖是通过直接依赖传递进来的,当升级直接依赖时,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,但有可能导致当前项目出错。这种潜在的威胁一旦出现,就往往需要耗费大量时间来查明真相

还有一个 Unused declared dependencies,表示项目中未使用的,但是显式声明的依赖。需要注意的是,对于这类依赖,不应该简单地直接删除其声明,而是应该仔细分析。由于 dependency:analyze 只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它就发现不了。所以在优化依赖时一定要小心测试

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,825评论 6 546
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,814评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,980评论 0 384
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 64,064评论 1 319
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,779评论 6 414
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,109评论 1 330
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,099评论 3 450
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,287评论 0 291
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,799评论 1 338
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,515评论 3 361
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,750评论 1 375
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,221评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,933评论 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,327评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,667评论 1 296
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,492评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,703评论 2 380

推荐阅读更多精彩内容

  • 依赖详解 groupId、artifactId、version依赖的基本坐标,对任何一个依赖来说坐标是最重要的,M...
    SonyaBaby阅读 570评论 0 2
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,836评论 18 139
  • 传递性依赖 传递性依赖是Maven2.0的新特性。假设你的项目依赖于一个库,而这个库又依赖于其他库。你不必自己去找...
    欧余山南阅读 1,242评论 0 0
  • 3.1 依赖的配置 一个依赖声明可以包含下面元素: groupId、artifactId、version依赖的基本...
    洛杨凡阅读 427评论 0 0
  • 一、NSOperation 1.简介NSOperation 实例封装了需要执行的操作和执行操作所需的数据,并且能够...
    Z_Han阅读 468评论 2 8