3.1 传递依赖:一棵树的蔓延方式


文档摘要

3.1 传递依赖:一棵树的蔓延方式 本节摘要:传递依赖指依赖的依赖,Maven 会沿依赖树递归解析直至叶子,声明 5 个依赖膨胀出两百多行树是常态而非异常。scope 与 optional 是控制蔓延的两道闸门。理解蔓延规则与拦截机制,是进入冲突定位与版本仲裁之前的第一课。 一、先看一次实测膨胀 给 risk-engine 数了数:POM 里 dependencies 段只有 5 个声明——spring-boot-starter-web、mybatis-plus、rule-sdk、commons-lang3、junit。跑一次依赖树: 5 个声明带来 146 个实际入场的 compile 构件。每个传递进来的构件又自带它的依赖清单,Maven 递归解析直到叶子节点,这个过程叫传递依赖解析。

3.1 传递依赖:一棵树的蔓延方式

本节摘要:传递依赖指依赖的依赖,Maven 会沿依赖树递归解析直至叶子,声明 5 个依赖膨胀出两百多行树是常态而非异常。scope 与 optional 是控制蔓延的两道闸门。理解蔓延规则与拦截机制,是进入冲突定位与版本仲裁之前的第一课。

一、先看一次实测膨胀

给 risk-engine 数了数:POM 里 dependencies 段只有 5 个声明——spring-boot-starter-web、mybatis-plus、rule-sdk、commons-lang3、junit。跑一次依赖树:

mvn dependency:tree | wc -l # 输出:217 行 mvn dependency:tree -Dscope=compile | wc -l # 输出:146 行 compile 作用域的传递依赖

5 个声明带来 146 个实际入场的 compile 构件。每个传递进来的构件又自带它的依赖清单,Maven 递归解析直到叶子节点,这个过程叫传递依赖解析。作战室视角必须先建立一个直觉:你声明的不是 5 个库,是 5 张社会关系网。任何一张网里的一次升级,都可能改变你 classpath 的构成——这就是"我没改代码但构建结果变了"的第一来源。

二、蔓延的两道闸门:scope 与 optional

传递不是无条件的,两道闸门控制着蔓延的范围。第一道是 scope(作用域),它同时决定依赖在哪个阶段可见、以及以什么身份向下传递

你的 scope 对下游的传递身份 战术含义
compile compile 全兵种通行 默认最泛滥
provided 不传递 编译要用 运行环境自带 如 Servlet API
runtime runtime 编译不需要 运行要用 如 JDBC 驱动
test 不传递 只在测试阶段出现
system 不传递 本地路径引入 高危不推荐
import 特殊 只用于 BOM 不引依赖只导入版本管理 第 3.3 节主角

关键结论有一条:provided 与 test 是传染阻断剂。一个库若把自己的某个依赖标为 provided,它就不会把这个依赖强加给你;反过来说,若你发现某构件"应该被传递却没有传下来",先查上游是不是标了 provided 或 optional。

第二道闸门是 optional(可选依赖),它声明"这个依赖是我的可选增强,不随我传递":

<!-- rule-sdk 的 POM:日志实现声明为可选 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.11</version> <!-- 可选:用我的人想要日志就得自己声明 --> <optional>true</optional> </dependency>

效果实测:risk-engine 引入 rule-sdk 后,依赖树里找不到 logback——它被 optional 闸门拦下了;直到 risk-engine 自己显式声明日志实现,它才入场。库作者用 optional 表达"插件式能力",应用侧则获得了选择权。经典例子是各种数据库驱动与连接池实现。

图:蔓延与拦截示意

图:蔓延与拦截示意

三、把蔓延管理起来

被动接受膨胀与主动管理膨胀,是两个工程段位的分界。三件事立刻能做。

第一件,定期数树。dependency:tree 的行数纳入构建观测,突然的增长意味着某个依赖做了一次"大扩军",值得在 code review 里问一句。

第二件,限制深度暴露。 公司内部的公共库应克制传递面:能标 optional 的标 optional,能收窄 scope 的收窄,工具类库尤其不要把整个日志实现、整个 HTTP 客户端强塞给使用方。

第三件,警惕"树很深"本身。 深度不只是数量问题,它与第 3.2 节的调解规则直接相关——路径深度是版本裁决的第一依据,一条意外变深的路径会改变裁决结果。实战中"升级了 A,B 的行为变了"的灵异事件,多半是 A 的新版本改变了某条传递路径的深度,让版本裁决换了胜者。

动手实验:亲手养大一棵树

空目录里建一个只有 POM 的项目,做一组对照实验,十分钟就能把蔓延机制内化:

<!-- 实验项目:先只引入一个 starter 级别的聚合包 --> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.14</version> </dependency> </dependencies>
mvn dependency:tree # 数一数行数 记下来 作为基线 # 然后再加上第二个依赖 重跑 再数 # 你会观察到:新增行数远大于 1 # 因为第二个包的传递依赖里 有一部分与第一个的传递依赖同名同版 被合并 # 也有另一部分是全新的 继续向下蔓延伸展

再做一个 optional 实验:给自己的项目写一个依赖声明、标记 optional,然后让另一个项目依赖你——观察 optional 的依赖没有出现在对方的树里。第 4.2 节讲 BOM 时,这些手感会直接复用。

实验做完顺手把 scope 的传播矩阵做成速查卡贴在作战室墙上(下表合并了两道闸门的全部组合,排查"该传的没传下来"时按行查):

上游声明的组合 会传给下游吗 典型例子
compile 且非 optional 传 且以 compile 身份 工具库的核心依赖
runtime 且非 optional 传 以 runtime 身份 JDBC 驱动
compile 且 optional 不传 可选日志实现
provided 无论 optional 不传 容器提供的 API
test 不传 测试框架

⚠️ 常见坑:为了"治住"膨胀,把大量依赖声明成 provided 或加一堆 exclusion。闸门是给库作者用的表达能力,不是应用侧的减肥药——provided 意味着运行环境必须真的提供,乱标会导致运行期缺类;乱 exclude 则会把别人需要的东西一并清场,第 3.2 节会给出排除的纪律。

本节要点回顾

  • 传递依赖是递归解析:声明的是关系网不是单个库,146 行树属于正常体量;
  • scope 决定传递身份:compile 全通行,provided 与 test 是传染阻断剂,import 是 BOM 专用;
  • optional 是库作者的声明:可选能力不外传,需要者自行显式声明;
  • 深度即权力:路径深度参与版本裁决,树形变化会静默改变裁决结果;
  • 管理三件事:数树观测、库作者收窄传递面、警惕异常加深。

蔓延的机制清楚了,当多条路径把同一构件的不同版本同时送进战场,裁决规则如何运转、爆炸如何定位——下一节正面开打。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U