关于后端:安全同学讲Maven间接依赖场景的仲裁机制

34次阅读

共计 7501 个字符,预计需要花费 19 分钟才能阅读完成。

简介:去年的 Log4j-core 的平安问题,再次把供应链平安推向了低潮。在供应链平安的场景,蚂蚁团体在动态代码扫描平台 -STC 和资产威逼透视平台 - 哈勃这 2 款产品在联结单干下,优势互补,很好的解决了间接依赖和间接依赖的场景。然而因为 STC 是基于事先,受限于扫描效率存在脱漏的危险面,而哈勃又是基于预先,存在修复工夫上的危险。基于此,笔者尝试寻找一种形式能够同时解决 2 款产品的短板。

作者 | 唐天龙 (唐礼) 起源 | 阿里开发者公众号一 背景为什么想写此文去年的 Log4j-core 的平安问题,再次把供应链平安推向了低潮。在供应链平安的场景,蚂蚁团体在动态代码扫描平台 -STC 和资产威逼透视平台 - 哈勃这 2 款产品在联结单干下,优势互补,很好的解决了间接依赖和间接依赖的场景。然而因为 STC 是基于事先,受限于扫描效率存在脱漏的危险面,而哈勃又是基于预先,存在修复工夫上的危险。基于此,笔者尝试寻找一种形式能够同时解决 2 款产品的短板。笔者尝试钻研了一下 Maven 是如何解决一个我的项目中的间接依赖和间接依赖的,并且在遇到雷同依赖时,Maven 是如何进行抉择的,这里的如何抉择其实就是 Maven 的仲裁机制。带着这些问题,笔者尝试调研了 Maven 的源码和做了一些本地的测试试验。总结了这篇文章。坐标是什么? 在空间坐标系中,咱们能够通过 xyz 示意一个点,同样在 Maven 的世界里,咱们能够通过一组 GAV 在依赖的世界里明确示意一个依赖,比方:< groupId> : com.alibaba 个别是公司的名称 < artifactId> : fastjson 项目名称 < version> : 1.2.24 版本号影响依赖的标签都有哪些 1.< dependencies> 间接引入具体的依赖信息。留神是不在 < dependencyManagement> 标签内的状况。如果是在 < dependencyManagement> 内的状况,请参考 2 号标签。2.< dependencyManagement> 只申明但不产生理论引入,作为依赖治理。依赖治理是指真正产生依赖的时候,再去参考依赖治理的数据。这样应用 dependency 的时候,能够缺省 version。另外 < dependencyManagement> 还能够管控所有的间接依赖,即便间接依赖申明了 version,也要被笼罩掉。3.< parent> 申明本人的父亲,Maven 的继承哲学跟 Java 很相似,因为 Maven 自身也是用 Java 实现的,满足单继承。一旦子 pom 继承了父 pom,那么会把父 pom 里的 < dependencies>,< dependencyManagement> 等等属性都继承过去的。当然如果在继承的过程中,呈现一样的元素,也是子去笼罩父亲,和 Java 相似。继承时,会分类继承。dependencies 继承 dependencies,dependencyManagement 里的依赖治理只能继承 dependencyManagement 范畴内的依赖治理。每一个 pom 文件都会有一个父亲,即便不申明 Parent,也会默认有一个父亲。和 Java 的 Object 设计哲学相似。前面在源码剖析中咱们还会提到。4.< properties> 代表以后本人的我的项目的一个属性的汇合。properties 仅仅代表属性的申明,一个属性申明了,和他是否被援用并无关系。我齐全能够申明一系列不被人应用的属性。依赖的作用域都有哪些一个依赖在引入的时候,是能够申明这个依赖的作用范畴的。比方这个依赖只对本地起作用,比方只对测试起作用等等。作用域一共有 compile,provided,system,test,import,runtime 这几个值。简略总结一下:compile 和 runtime 会参加最初的打包环节,其余的都不会。compile 能够不写。test 只会对 src/test 目录下的测试代码起作用。provided 是指线上曾经提供了这个 Jar 包,打包的时候不须要在思考他了,个别像 serlvet 的包很多都是 provided。system 和 provided 没什么太大的区别。import 只会呈现在 dependencyManagement 标签内的依赖中,是为了解决 Maven 的单继承。引入了这个作用域的话,maven 会把此依赖的所有的 dependencyManagement 内的元素加载到以后 pom 中的,但不会引入以后节点。如下图,并不会引入 fastjson 作为依赖治理的元素,只是会把 fastjson 文件定义的依赖治理引入进来。

二 单个 Pom 树的依赖竞争 Pom 文件实质一个 Pom 文件的实质就是一棵树。在人的视角来察看一个 Pom 文件的时候,咱们会认为他是一个线状的一个依赖列表,咱们会认为下图的 Pom 文件形象进去的后果是 C 依赖了 A,B,D。但咱们的视角是不齐备的,Maven 的视角来看,Maven 会把这一个 Pom 文件间接形象成一个依赖树。Maven 的视角能看到除了 ABD 之外的节点。而人只能看到 ABD 三个节点。既然是在一棵树上,那么雷同的节点就必然会存在竞争关系。这个竞争关系就是咱们提到了仲裁机制。

Maven 仲裁机制准则 1. 依赖竞争时,越凑近骨干的越优先。2. 单颗树在依赖在竞争时 (dependencies)(留神:不是 dependencyManagement 里的 dependencies):当 deep=1,即间接依赖。同级是靠后优先。当 deep>1,即间接依赖。同级是靠前优先。3. 单颗树在依赖治理在竞争时(留神:是 dependencyManagement 里的 dependencies) 是靠前优先的。4.maven 里最重要的 2 个关系,别离是继承关系和依赖关系。咱们所有的法则都应该只从这 2 个关系动手。下图中别离是 2 个子 pom 文件(方块代表依赖的节点,A-1 示意 A 这个节点应用的是 1 版本,字母代表节点,数字代表版本)。右边这个子 pom 生成的树依赖了 D-1,D- 2 和 D -5。满足依赖竞争准则 1,即越凑近树的左侧越优先的准则,所以 D - 5 会竞争胜利。然而 B - 1 和 B - 2 同时都位于树的同一深度,并且深度为 1,因为 B - 2 更加靠后,所以 B - 2 会竞争胜利。左边的子 pom 生成的树依赖了 D- 1 和 D -2,并且位于同一深度,但因为 D - 1 和 D - 2 是属于间接依赖的范畴,deep 大于 1,所以是靠前优先,那么也就是 D - 1 会竞争胜利。

常见场景看到这里,想必大家曾经理解了 Maven 的仲裁准则。然而在理论的工作中,光有准则还须要在代码中能够灵便的使用能力有属于本人的了解,这里笔者筹备了 5 个场景,每个场景对应的答案都在前面,大家浏览时,能够本人尝试用 Maven 的准则来去推理,看看有没有哪里不合乎预期的状况。场景一 难度 (☆) 场景形容主 POM 里有 < fastjson.version> 这个属性为 1.2.24。父亲是 spring-boot-starter-parent-3.13.0。父亲里的 < fastjson.version> 是 1.2.77。并且在主 pom 中,生产了这个属性。那么针对主 POM 这颗树,他最终会是应用哪一个 fastjson 呢?场景示例

结构图

场景二 难度 (☆☆) 在同一个主 POM 或者子 POM 中的 dependencies 中同时应用了 Fastjson,第一个申明了 1.2.24 的版本,第二个申明了 1.2.25 版本。那么针对主 POM 或者子 pom 这棵树,最终会抉择 fastjson 1.2.24 还是 1.2.25 呢?场景示例

结构图

场景三 难度 (☆☆☆) 下图中左图为主 POM 文件内的 dependencyManagement 里的 fastjson 为 1.2.77,这个时候子 POM 中显示申明本人的版本 1.2.78。那么针对子 POM 这颗树,子 POM 会抉择服从父命还是听从心田呢?场景示例

结构图

场景四 难度 (☆☆☆☆) 主 POM 的 dependencies Fastjson:1.2.24 主 POM 的 dependencymanagent Fastjson:1.2.77 主 POM 的父亲(springboot)的 dependencies Fastjson 1.2.78 子 POM 里的 dependencies Fastjson 1.2.25 这种状况下针对子 pom 来说,他会抉择 4 个版本中的哪一个呢?场景示例

结构图

场景五 难度 (☆☆☆☆☆) 主 POM 的 dependencies Fastjson:1.2.24 主 POM 的 dependencymanagent Fastjson:1.2.77 主 POM 的父亲(springboot)的 dependencies Fastjson 1.2.78 子 POM 里的 dependencies 不写 version 场景五跟场景四整体没有差异,只是将子 pom 的 dependencies 的版本进行缺省。这种状况下针对子 pom 来说,针对子 pom,他会抉择 3 个版本中的哪一个呢?场景示例

结构图

答案场景一 1.2.24 会最终失效。因为子会继承父亲的属性,然而因为本人有这个属性,那么则笼罩!继承肯定会随同着笼罩的,这个设计在编程语言中还是比拟广泛的。场景二 1.2.25 会最终失效。参考 单颗树在依赖在竞争时:当 deep=1,即间接依赖。同级是靠后优先。满足 Maven 的外围竞争依赖策略!场景三 1.2.78 最终会失效。一个我的项目里的 dependencyManagement 只能对不申明 version 的 dependency 和间接依赖无效!场景四 1.2.25 会最终失效。这个比较复杂。〇: 首先依据父子的继承关系,1.2.24 会笼罩掉 1.2.78。所以 78 版本淘汰一: 因为一个我的项目里的 dependencyManagement 只能对不申明 version 的 dependency 和间接依赖无效,所以 1.2.77 无奈对 1.2.25 起作用。二: 因为父子的继承关系,1.2.25 会笼罩掉 1.2.24. 所以最终 1.2.25 胜出!场景五 1.2.77 会最终失效。〇: 首先依据父子的继承关系,1.2.24 会笼罩掉 1.2.78。所以 78 版本淘汰一: 因为一个我的项目里的 dependencyManagement 是能够对不申明的 version 起作用,所以子 pom 的版本为 1.2.77 二: 因为父子的继承关系,1.2.77 会笼罩掉 1.2.24. 所以最终 1.2.77 胜出!三 多个 Pom 树合并打包多棵树构建程序准则当初的我的项目个别都是多模块治理,会存在十分多的 pom 文件。多棵树的状况下每棵树的出场程序都是当时曾经被计算好的。这个性能在 Maven 的源码中是一个叫 Reactor(反应堆)实现的。它次要做了一件事件就是决定一个我的项目中,多个子 pom 谁先进行 build 的程序,这个出厂程序很重要,在合并打包时,往往决定了最终谁会在多个 pom 之间胜出的问题。Reactor 的准则多棵树(多个子 pom)构建的程序是依照被依赖方的要在前,依赖方在后的准则。我的项目要保障这里是不能呈现循环依赖的。Reactor 的准则图解如下图子 pom1 在被子 pom2 和子 pom3 同时依赖,所以子 pom1 最先被构建,子 pom3 没有人被依赖,所以最初构建。

SpringBoot Fatjar 打包的策略 SpringBoot 打包会打成一个 Fatjar, 所有的依赖都会放在 BOOT-INF/lib/ 目录下。SpringBoot 的打包是越靠后的构建 pom 越优先,因为个别会把 springboot 的打包插件放在最不被依赖的 module 里(比方上图里的 Pom3)。(SpringBoot 的打包插件个别放在 bootstrap pom 里, 这个名字能够咱们本人起,个别都是依赖关系最考上的 module。在多模块治理的 springboot 利用内,bootstrap 往往是最不被依赖的那个 module。)

子 pom3 最初参加构建,而且 SpringBoot 打包插件个别打的就是这个 module。所以最终进入到 SpringBoot 打包产物的有 A -2,B-2,E-2,F- 2 和 D -1。因为 A - 2 和 B - 2 相比于其余几个雷同节点更凑近树的骨干。E- 2 和 F - 2 也是同理。这个法则体感上是靠后优先了,因为靠后的树人造更加凑近骨干。

四 仲裁机制在 Maven 源码中的实现以 Maven 的 3.6.3 版本的源码进行剖析,咱们尝试剖析 Maven 中对依赖解决的几处准则,方能从源码的层面上正向的证实仲裁机制的准确性。另外从源码上也能够看出一些 Maven 上的机制为什么是这样,而不是单单的他的机制是什么样。因为笔者置信,任何机制都无奈保障与时俱进下的先进性,所以笔者认为上文中提到的所有的仲裁机制有一天可能会发生变化,这些论断并非最重要,而是如何调研这些论断更为重要!Maven 是如何实现出继承并且雷同属性子笼罩父的 Maven 中有 2 条十分重要的主线。一个是依赖,另一个就是继承。Maven 在源码中实现继承大体如下。在下图中应用 readParent 进行对父亲的模型获取之后,便让本人陷入这个循环中。惟一能够进来这个循环的形式就是追不到父亲为止。并且把每次取到模型数据放到 linega 这个对象当中。下图中最上面的 assembleInheritance 咱们看他生产了 linega 这个对象,目标就是实现实在的继承和笼罩。

在 assembleInheritance 中咱们会发现一个很有意思的景象,lingage 是倒着进行遍历,并且是从倒数第二个元素开始,这正是上文中咱们提到了的 Maven 的一个设计哲学。Maven 认为这个世界上所有的 pom 文件都存在一个父亲,相似 Java 的 Object。这里便是对这个哲学解决的一个浅逻辑。另外 Maven 自上而下的去遍历,更加不便本人去实现雷同的元素子笼罩父的能力,这也是笔者认为在编码上的一个小心理。

Reactor 反应堆在源码中的实现上文中咱们还提到了一个十分重要的概念,就是反应堆。反应堆间接决定了各个子 pom 是如何决定构建程序的。在 Maven 的源码中,他是在 getProjectsForMavenReactor 函数中进行实现的。并且咱们从下图中也能够看到,Maven 的反应堆是不能解决循环依赖的,他间接捕捉了这种异样!

真正实现反应堆算法的是在 ProjectSorter 的构造函数中通过 Dag 进行实现的。Dag(有向无环图)和广度优先搜寻是解决依赖场景是一个很好的形式。在有向无环图中通过每次筛选出入度为 0 的节点,再删除该节点和此节点的相邻边,一直反复上述步骤。就能够高效率的计算出 DAG 上的所有节点的依赖程序,Maven 也正是用到了这个思路。从这个源码的视角也能够解释为什么 Maven 必须要保障每一个子 pom 之前不能呈现循环依赖。

同一个 Pom 文件内 dependency 后申明的优先的实现在解决 Dependencies 时,Maven 并没有对此进行非凡解决,是间接应用的 Map 的形式进行笼罩的。对于这里为什么这么设计,笔者并不分明。笔者曾一度猜想这么设计是为了让开发同学更好的编写,因为靠后优先往往合乎大部分人的编码习惯。然而在这里咱们看到了作者的一行正文,意思大略是说,这样设计是为了向后兼容 Maven2.x,因为 Maven2.x 是不会去校验一个文件是否只存在一个同 GA 的惟一依赖。所以前面的 maven 的版本应该也是连续了这种格调。

当循环进行解决到 1.2.25 的时候,仍然进行对 normalized 这个 map 进行 put 操作导致了 key 值雷同的状况下的笼罩。

五 平安视角应如何防止间接依赖剖析作为平安同学,笔者更心愿的是针对这种多 module 的 Maven 我的项目能够梳理出一个教训,怎么去防止间接依赖的问题。通过下面的剖析,咱们能够得出 3 条论断:1. 子 pom 申明版本在平安视角是十分危险的,子 pom 不应该显示申明版本。因为子 pom 会继承主 pom 的元素,并且在继承的时候会呈现笼罩的场景。那么针对 CE 或者 SpringBoot 打包时,有可能呈现子 pom 的 build 的程序地位人造十分有劣势,容易造成子 pom 的版本进入最终的打包产物。2. 主 POM 的 dependencyManagent 能够管控到 间接依赖 和 不显示申明 version 的间接依赖。3. 主 POM 的 dependencies 不能呈现危险版本。否则子 pom 人造的继承了这个危险版本参加打包。论断以上几条同时满足,便能够解决间接依赖的问题。即:针对 SpringBoot 而言,子 pom 不应该显示申明版本,主 Pom 的 dependencyManagent 应该管控平安版本的依赖,并且主 pom 不能呈现危险版本。(主 Pom dependencies 强行写上平安版本更佳,这样能够防止掉依赖的父亲里存在残留的不平安的依赖)六 最初 Maven 的源码地址 https://archive.apache.org/di… 我是怎么剖析的自己在本地针对 SpringBoot,做多轮测试。在根目录下执行 mvn clean package 即可!mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true 会帮忙剖析到具体的节点。另外就是尝试在源码中找到这里的实现,这样更能加深了解!罕用的剖析命令 0.mvn clean package -DSkipTest 间接进行打包,进行后果剖析 1.mvn dependency:tree 会把整个的 maven 的树形构造输入 2.mvn help:effective-pom -Dverbose 这个命令输入的信息更加残缺,输入的是 effectivepom3.mvn clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:tree -Dverbose=true4.mvn -D maven.repo.local = 你的目录 compile 阶段用到的依赖。举荐浏览 1. 如何写出一篇好的技术计划?2. 阿里 10 年积淀|那些技术实战中的架构设计办法 3. 如何做好“防御性编码”?阿里云产品测评—开源 PolarDB-PG 体验阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容 Oracle 语法;采纳基于 Shared-Storage 的存储计算拆散架构,具备极致弹性、毫秒级提早、HTAP 的能力和高牢靠、高可用、弹性扩大等企业级数据库个性。公布评测,写下你的感触与评估即可取得多重福利。点击这里,查看详情。原文链接:https://click.aliyun.com/m/10… 本文为阿里云原创内容,未经容许不得转载。

正文完
 0