什么是 JAR 地狱?

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

什么是罐子地狱? (或者是类路径地狱?还是依赖地狱?)在考虑像 maven 或 osgi 这样的现代开发工具时,哪些方面仍然相关?

有趣的是,这些问题似乎没有结构化的答案(即即使 第二页也 没有列出有希望的标题)。这篇文章应该填补这一空白。

概述

我们将从构成 jar hell 的问题列表开始,暂时忽略构建工具和组件系统。当我们评估当前事态时,我们将在第二部分再次讨论它们。

罐地狱

jar hell 是一个可爱的术语,指的是由于java类加载机制的特点而产生的问题。其中一些建立在彼此之上;其他人是独立的。

未表达的依赖

jar 无法以 jvm 可以理解的方式表达它所依赖的其他 jar。需要一个外部实体来识别和实现依赖关系。开发人员必须通过阅读文档、找到正确的项目、下载 jar 并将它们添加到项目中来手动执行此操作。可选依赖项,如果开发人员想要使用某些功能,一个 jar 可能只需要另一个 jar,这进一步使过程复杂化。

运行时在需要访问它们之前不会检测到未满足的依赖项。这将导致 noclassdefounderror 使正在运行的应用程序崩溃。

传递依赖

要使一个应用程序正常工作,它可能只需要少数几个库。其中每一个都可能需要一些其他的库,等等。随着未表达的依赖关系问题的复杂化,它变得更加劳动密集且更容易出错。

阴影

有时类路径上的不同 jar 包含具有相同完全限定名称的类。这可能由于不同的原因而发生,例如,当同一个库有两个不同的版本时,当一个 fat jar 包含也作为独立 jar 引入的依赖项时,或者当一个库被重命名并在不知不觉中两次添加到类路径时。

由于类将从类路径上的第一个 jar 加载以包含它们,因此该变体将“遮蔽”所有其他变体并使它们不可用。

如果这些变体在语义上有所不同,这可能会导致任何事情,从过于微妙以至于无法引起注意的不当行为到造成严重破坏的错误。更糟糕的是,这个问题表现出来的形式似乎是不确定的。这取决于搜索罐子的顺序。这在不同的环境中可能会有所不同,例如在开发人员的 ide 和代码最终将运行的生产机器之间。

版本冲突

当两个必需的库依赖于第三个库的不同、不兼容版本时,就会出现此问题。

如果两个版本都存在于类路径中,则行为将不可预测。首先,由于阴影,存在于两个版本中的类只会从其中一个版本中加载。更糟糕的是,如果一个类存在于其中一个中但另一个中不存在,则该类也将被加载。因此调用库的代码可能会发现这两个版本的混合。

由于需要不兼容的版本,如果缺少其中一个版本,程序很可能无法正常运行。同样,这可能表现为意外行为或 noclassdefounderrors。

复杂的类加载

默认情况下,所有应用程序类都由同一个 类加载器 加载,但开发人员可以自由添加其他类加载器。

这通常由组件系统和 Web 服务器等容器完成。理想情况下,这种隐式使用对应用程序开发人员是完全隐藏的,但正如我们所知, 所有抽象都是有漏洞的 。在某些情况下,开发人员可能会显式添加类加载器来实现功能,例如允许他们的用户通过加载新类来扩展应用程序,或者能够使用相同依赖项的冲突版本。

不管多个类加载器是如何进入画面的,它们都可以迅速导致一个复杂的机制,显示出意想不到的和难以理解的行为。

类路径地狱和依赖地狱

classpath hell 和 jar hell 本质上是一回事,尽管后者似乎更侧重于复杂类加载器层次结构引起的问题。这两个术语都是特定于 java 和 jvm 的。

另一方面, 依赖地狱 是一个使用更广泛的术语。它描述了软件包及其依赖性的一般问题,适用于操作系统以及个人开发生态系统。鉴于其普遍性,它不涵盖特定于单个系统的问题。

从上面的列表中,它包括传递的和可能未表达的依赖关系以及版本冲突。类加载和隐藏是 Java 特定的机制,不会被依赖地狱所涵盖。

惠康图书馆 cc-by 4.0 发布

事态

构建工具

查看问题列表,我们可以看到构建工具如何帮助解决其中的一些问题。他们擅长使依赖关系显式化,这样他们就可以沿着传递依赖树的无数边缘寻找每个需要的 jar。这在很大程度上解决了未表达和传递依赖的问题。

但是 Maven 等人。对阴影不做任何事情。虽然他们通常致力于减少重复类, 但他们无法阻止它们 。构建工具也无助于解决版本冲突,除非指出它们。并且由于类加载是一个运行时构造,因此它们也没有涉及它。

组件系统

我从未使用过像 osgi wildfly 这样的组件系统,所以我无法证明它们的工作情况。从他们声称的内容来看,他们似乎能够解决 jar hell 的大部分问题。

但是,这会带来额外的复杂性,并且通常需要开发人员更深入地研究类加载器机制。具有讽刺意味的是,也是上面列表中的一点。

但不管组件系统是否确实大大减轻了 jar hell 的痛苦,我的印象是绝大多数项目都没有使用它们。在这种假设下,绝大多数人仍然遭受与类路径相关的问题。

这会给我们留下什么?

因为它们没有被广泛使用,所以组件系统不会影响全局。但是构建工具的普遍存在极大地改变了 jar hell 不同圈子的严重性。

没有构建工具支持我参与或听说的项目花费了大量时间来处理未表达或传递依赖项的问题。阴影不时地抬起它丑陋的头,需要不同的时间来解决——但它最终总是如此。

版本冲突是 jar hell 中最有问题的一个方面。

但是每个项目迟早都会与冲突版本的依赖性作斗争,并且不得不做出一些艰难的决定来解决这些问题。通常一些需要的更新必须推迟,因为它会强制执行当前无法执行的其他更新。

我敢说,对于大多数规模适中的应用程序、服务和库,版本冲突是决定何时以及如何更新依赖项的主要决定因素之一。我觉得这无法忍受。

我对非平凡的类加载器层次结构的经验太少,无法评估它们有多少反复出现的问题。但考虑到到目前为止我所从事的项目都不需要它们,我敢说它们并不常见。在网上搜索使用它们的原因通常会出现我们已经讨论过的问题:导致版本冲突的依赖关系。

所以根据我的经验,我会说版本冲突是 jar hell 中最有问题的一个方面。

反射

我们已经讨论了 jar hell 的组成部分:

  • 未表达的依赖
  • 传递依赖
  • 阴影
  • 版本冲突
  • 复杂的类加载

基于构建工具和组件系统给游戏带来的影响以及它们的使用范围,我们得出结论,未表达和传递的依赖关系在很大程度上得到了解决,至少掩盖了轻松和复杂的类加载并不常见。

这使得版本冲突成为 jar hell 中最有问题的方面,影响了大多数项目中的日常更新决策。