简介: 去年的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...本文为阿里云原创内容,未经容许不得转载。