写点什么

Maven 揭秘,逃离依赖地狱

  • 2023-07-03
    北京
  • 本文字数:3649 字

    阅读完需:约 12 分钟

Maven揭秘,逃离依赖地狱

英国 Devoxx 的演讲中,JFrog 的开发倡导者 Ixchel Ruiz 和 Oracle 首席产品经理 Andres Almiray 共同介绍了多个“Maven 难题”,以及摆脱阿帕奇 Maven “依赖地狱”的可能解决方案。这次演讲中涉及了直接、传递、父级 POM,以及物料清单(BOM)的导入。


Ruiz 以对工具价值的思考展开了演讲:


Ruiz:作为开发者,如果我的工具在做它该做的,我会感觉很好。


在清楚了解“好工具”时,“魔法”就会发生。正如标题所述,这次演讲主题时关于构建工具,更确切地说,是关于阿帕奇 Maven 的构建工具。


据 JFrog 制品仓库(Artifactory)的统计数据和 JRebel 或 JetBrains 的开发者生产力调查,Maven 仍是主流 Java 开发者的构建工具,市场份额分别占据 68%和 73%。


作为 Gradle 的长期支持者,Almiray 称即使是 Gradle 或 sbt 也或多或少地依赖 Maven。因此,即使是想要以自己的方式编码,人们仍然需要通过 POM 格式解决依赖关系。而有时这些东西又不会如人预期一般正常工作,或是说像是经典的 Maven 一样运作。


在一个新克隆的 maven 项目上,你首先会做什么?演讲者以这个问题为切入,开始了对问题的回答。Almiray 鼓励观众在除项目依赖于其他远程项目的情况下,将本能的“mvn clean install”替换为“|mvn verify”,因为“检查(verify)”是 Maven 生命周期中“安装(install)”的上一步,是通过构建并运行测试进行项目验证的。而安装则仅仅是将构建(编译和打包)结果从文件系统(构建的位置)复制到仓库。



随后,演讲进入了互动问卷环节,由听众对问卷内容进行实时回复。在问题的描述中,如果依赖的坐标(即 groupId、artificatId)相同但版本号不同,那么麻烦就会出现。


主持人在“暖场问题”中确定了听众所使用的 Maven 版本、安装方式,以及是否使用 Maven 守护进程(daemon)。针对听众的选择,二位演讲者建议使用 SDKMAN(即使是在两个不同终端窗口内,也允许使用两套不同版本的 Java),用于提升速度的 Maven 守护进程,以及新版本 Maven 3.9.x 以进入更为颠覆性的 4.x 版本


在演讲的问答阶段,二位演讲者以谷歌 Guava 依赖为例,但理由不是因为“大家都恨 Guava,而是因为大家都用 Guava”。他们提出了多个场景、问题、解答,以及优化和解决方案的相关建议,并将场景分为了三部分:单 POM 文件的依赖性、对父 POM 文件的依赖性,以及 BOM 导入。

单 POM 文件的依赖管理


第一种情况,连续声明了两个不同版本 Guava 的简单项目,哪一个会被采用?听众的回答几乎是在两个版本和构建错误之间五五开。虽然这些看起来仅仅是个简单的规则应用,但由于目前存在的众多版本和插件组合,事情往往会变得更加复杂。比如,这种情况在 Maven 4.x 中会出现构建错误,但在 3.x 中仅会以警告的形似出现。



这个问题的答案是,在 28.0 版本的 Guava 中会被解决,但因为该版本号不够高,Maven 永远会采用最后一个声明的依赖。在加上 Maven 无法理解版本号,这些在它眼里仅仅只是字符串。为确保这些情况不会再发生,Ruiz 和 Almiray 建议在 Maven 4.x 版本普及之前采用 Maven 增强(enforcer)插件中的“禁止重复 POM 版本号规则”。首次使用这个插件时大概会非常痛苦,因为该规则会生成一个构建错误,迫使你为项目选择合适的版本。


下一个问题是对上一个问题的改版,将第二个依赖换为了更高版本的传递性依赖。即使第二个也是 POM 文件里最后声明的依赖版本号更高,直接的“依赖版本总会赢”。Almiray 提及阿帕奇 Maven 的前主席 Robert Scholte 曾强调,Maven 这项工具无法理解语义上的版本号划分,它只认得依赖在“图中的位置”。此外,依赖图中同一依赖的不同版本可能会导致应用程序时不时的崩溃。为避免这类情况的发生,可使用 Maven 增强插件中的“依赖收敛规则”,以确保版本号的一致。如果需要强调版本号在语义上的一致性,可使用增强规则“需求依赖项上界”,该规则可给出依赖图中的可用新版本。将两项规则相结合后,就能得知同一依赖的两个版本,以及依赖的可用新版本。


第二种情况则引入了依赖管理的概念,其原理更像是查找表。在 Maven 构建图时,会在表中搜索匹配的依赖(artifactId 及 groupId),并选择其所定义的版本。第一个例子中只有依赖管理块和通过谷歌 Truth 获取的横向依赖,而第二个例子中则额外增加了直接依赖的内容。在例子一中,定义在依赖管理块中定义的版本号会“赢”,而例子二中则是直接依赖“赢”,也就是说“无论是在图中哪里定义的,直接依赖总会赢”。

另一个例子中则使用了两个依赖,二者均带来了与根距离相同的传递性依赖(“这点很重要”),而再将依赖管理块加进来又会让情况有所不同。听众们认为可以将直接依赖的情况套用,所以图中最后定义的依赖会“赢”,但传递性依赖的情况却是恰恰相反,即第一个库中、第一且最近的传递性依赖将“赢”。在这种情况下,由于这两个依赖距离相同,所以 Guice 带来的横向依赖(Guava 30.1)会赢。在依赖块加入后,其所定义的版本号将“赢”。

父 POM 文件依赖


在这段演讲中,二位演讲者又在依赖管理中加入了新的复杂因素:父 POM。每个 POM 都可以有一个父 POM,并为依赖管理带来不同的背景。父级关系是在子级层面定义的,父级不会知道任何继承自己子级的信息。每个 POM 都会有一个父级,如果没有明确定义,那么父级将默认成为 super POM。而 Maven 之所以能在只有基本插件的情况下构建项目,是因为 super POM 中包含了所有需要插件。


Almiray:单个 POM 文件很好处理,但难免会出现多个 POM 文件的情况。

Ruiz:当然,这时候事情就好玩了。


在另一个例子中,情况一是父系中定义一个直接依赖,子系中定义一个传递依赖;情况二中则新增了一个依赖管理块。因为父 POM 是直接导入到子 POM 中,我们可以随时回到前一阶段,即有直接依赖、传递依赖,以及依赖块的单 POM,而正如所料,“直接依赖总会赢”。唯一的不同则是生效的 POM 同时也会导入父 POM 中的内容。因此,依赖是否能被解决取决于其在生效 POM 中的位置。

BOM 依赖


演讲中使用的最后一个概念是物料清单(BOM),是与别称“安全物什”的软件物料清单(SBOM)不同的。虽然没有对 BOM 的官方定义或分类,但据 Almiray 的说法,BOM 可以分为以下两类:

  • 库 BOM:定义了项目与单一库的关联。举例来说,JUnit 或 Jackson BOM 定义且仅定义了一切与 JUnit 相关。

  • 堆栈 BOM: Spring 或 Quarkus BOM 可当前项目提供其运行所需的各个项目中的全部依赖关系,每个 DOM 中都包含有最合适运行的依赖版本组合。


Ruiz:就算是没用过,也一定消费过(BOM)。如果用 Spring Boot 或 Quarkus 导入东西,必然会带来更多的依赖……


无论你用的是哪类 BOM 最终的消费形式都是一样的:通过依赖管理块(Ruiz 称这是依赖管理块存在的意义)。消费 BOM 不仅需要 artifactID 和 groupID,还需要添加 POM 类型,否则 Maven 只会试图将其解析为 JAR 文件。POM 文件之中只有元数据。



下面这个问题是前一个的变体。其中包含一个父级 POM 一个子级 POM,生效的 POM 有两个过渡性依赖,父级中的依赖管理模块有一个依赖和一个导入 BOM 的依赖(如上图所示)。另一个问题中则又在其中加入了直接依赖(无图)。生效的 POM 文件将引用父级中定义的依赖块,而 Guava 所选择的版本也是在这个依赖块中定义的。因此,第一个问题中生效的会是 28.0-jre。但对第二个问题而言,演讲者们给自己不断强调的“直接依赖总会赢”的说法打上了补丁,“……除非 BOM 文件是导入的,否则其他都会被忽略”。也就是说,情况二中依赖管理块中使用的版本是 26.0-jre。


点击查看大图


另一种情况中(如上图),父级和子级 POM 中都有定义依赖管理模块。子级的 POM 中包含导入的 BOM 和依赖。在这种情况下,子级定义的版本与所用的版本更接近,因此会直接覆盖父级中所定义的依赖版本。也就是说,子级 POM 的依赖块中定义的依赖会被使用,即 26.0-jre。如果父级中有一个依赖块,子级中的依赖块导入了一个 BOM、一个传递性依赖和直接依赖,那么直接依赖将会笑到最后,即 28.0-jre。


在演讲的最后,Ruiz 和 Almiray 给出了演讲中的关键点。她提到了依赖的采用在很大程度上取决于其在 POM 文件内定义的位置,并再次强调了使用 Maven 增强插件的重要性,以确保明确规则的定义和遵循。因此,在依赖定义文件中的不同位置新增一个条目并不会改变构建过程的输出。


Almiray:如果要从这次演讲中学到什么,那就是去使用 Maven 增强插件,并从今天开始,正确地开始项目构建……


Ruiz 在结束语中再次重申了 Maven 依赖的解决规律:直接依赖总会赢,传递性依赖会选择最先也是最近的。依赖管理解析会以目录的形式使用。对 BOM 文件而言,就算不知道也在用。Almiray 最后称,万不得已可以用排除法,只要用 Maven 构建,依赖管理块就是好工具。但如果用不同的消费者,你就需要将所有不同类型的依赖块转换为显示依赖。Maven flatten 可以做到这点。对 Gradle 而言则更是需要,因为 Gradle 除了直接依赖关系之外,不支持任何其他东西。


原文链接

Ruiz and Almiray at Devoxx UK: Lessons on How to Escape the Maven Dependency Hell

2023-07-03 11:201924

评论

发布
暂无评论
发现更多内容

物联网资产整合架构

老任物联网杂谈

物联网架构

【Howe 学 JAVA】Java 类集框架1——List集合

Howe

Java List 集合

《Linux就该这么学》笔记(一)

编程随想曲

Linux

Mac 自带软件-聚焦搜索

Winann

macos Mac spotlight

CTO股权”避坑“,你根本不知道我们多努力

赵新龙

TGO鲲鹏会 股权 CTO

深入理解MDL元数据锁

Simon

MySQL

面试官竟然一直和我聊线程的启动和终止

Simon郎

Java 大数据 后端 多线程

TL如何在团队中培养出更多前端技术专家

贵重

大前端 团队建设 技术管理

《CSS 选择器世界》读书笔记

云走

CSS Java html 读书笔记 大前端 张鑫旭

保险知识梳理

魁拔

保险 生活质量

MacOS使用指南之我并不需要系统菜单栏

lmymirror

macos 高效工作 完美主义 操作系统 新手指南

你觉得你是哪类人?

Janenesome

读书笔记 思考

自助设备系列——技术应用

孙苏勇

产品 行业资讯 智能设备

回文串解题记录

晓刚学代码

Java 算法

高仿瑞幸小程序 06 layout布局

曾伟@喵先森

小程序 微信小程序 大前端

回"疫"录(13):不信谣,不传谣

小天同学

疫情 回忆录 现实纪录 纪实 谣言

我的编程之路-3(熟练)

顿晓

c++ 调试 经历 项目 疑问

前端开发的瓶颈与未来之路

keelii

node.js typescript ruby-on-rails 编程 大前端

我跑步的时候会想些什么

养牛致富带头人

跑步 运动 锻炼

找到自己的领域,然后封神

一尘观世界

成长 提升 领域 机遇 趋势

【Howe 学 JAVA】Java 类集框架2——Set 集合

Howe

Java 集合 set

带你100% 地了解 Redis 6.0 的客户端缓存

程序员历小冰

redis 缓存 redis6.0.0

人生就是一场说走就走的旅行

kimmking

Web3极客日报#137

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

可能是最最最最简单的搭建博客方法

彭宏豪95

GitHub 写作 博客 GitPress

Web3极客日报#136

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

办公人员的 python 妙用——抽签结果提取

小匚

Python 远程办公

游戏夜读 | 游戏设计需要天赋?

game1night

OceanBase原理与实现分析

ElvinYang

【Howe 学 JAVA】Java 类集框架2——集合输出

Howe

Java 集合 输出 类集

CentOS7使用Iptables做网络转发

wong

Centos 7 iptables

  • 扫码加入 InfoQ 开发者交流群
Maven揭秘,逃离依赖地狱_编程语言_Olimpiu Pop_InfoQ精选文章