java hell 转载

转载:https://toutiao.io/posts/452124/app_preview

 

Hell文章的中文翻译。由于译者翻译水平有限,译文难免会有纰漏,所以英文好的童鞋最好还是去读原文。同时也欢迎大家对译文提出宝贵的意见。

什么是JAR地狱(JAR Hell)?什么又是CLasspath地狱和依赖地狱? 即使是采用了像Maven或OSGi这样的现代化开发工具,还有哪些方面是与之相关的呢?

有趣的是,这些问题似乎现在还是没有比较系统结构化的答案。这篇文章就是为了填补这方面的空白。

概述
首先我们会从构成JAR地狱的一系列问题说起。现在暂且让我们抛开构建工具和组件系统不谈,在第二部分探讨这现象的行业现状时会深入讨论这些方面的问题。

JAR地狱
JAR地狱是一个诙谐的说法,指的是由Java类加载机制的特性引发的一系列问题。其中一些问题是彼此相互影响,而其他的则是独立的。

依赖往往难以言明
一个JAR包无法以Java虚拟机(JVM)能够理解的方式来描述它依赖于哪些JAR。因此,想要识别和满足这些依赖就需要来自于外部的手段。作为开发者而言,他需要手动地查阅指定文档,找到正确的项目并下载相应的JAR文件,然后将这些JAR添加到自己项目中去。此外还有一种情况会使得这个过程更加复杂,那就是可选的依赖。也就是说只有需要使用特定功能时,一个JAR包才会依赖的别的JAR包。

对于那些无法满足的依赖,Java运行时系统只有在真正需要访问这些依赖时才能检测到该情况。这时运行时系统将会抛出NoClassDefFoundError异常,进而导致正在运行的应用程序崩溃。

依赖具有传递性
一个应用程序可能只需要依赖几个类库就能正常工作。而每一个类库又可能依赖于其他的一些类库,以此类推。这与依赖难以表述的问题结合起来,会让情况变得异常复杂,并将使得人力消耗和出错的可能性呈几何倍数增长。

屏蔽
有时classpath中不同的JAR包会包含限定名完全相同的类。造成这种现象的原因有很多,例如一个类库存在两个不同的版本,例如一个包含了所有依赖的Fat JAR(译者注:Fat JAR是将所有依赖和资源文件都打包成一个JAR包)又被当成standalone的JAR来使用,例如一个类库被重命名后又再次被加到classpath中。

JVM总是从classpath中第一个包含某个类的JAR包里加载该类,这个被加载的类将会“屏蔽”该类的其他版本,使这些类变得不可用。

如果这些不同版本的类在语义上有所区别,将会导致各类问题,从难以发现的不正常行为到非常严重的错误都可能发生。更糟糕的是这类问题的表现形式很可能是不确定。这取决于JVM查找JAR包的顺序。因此,问题的表现形式很可能因环境的不同而有差别。典型的例子就是开发者使用的IDE和生产环境的不同将可能让相同的代码产生不同的行为。

版本冲突
当两个必须的类库各自依赖于不同版本且不兼容的第三个类库时就会产生这个问题。

如果某个类库的两个不同版本都存在于classpath中,那么应用程序的行为将变得无法预测。首先,由于存在屏蔽问题,两个版本中都存在的类只能从其中一个类库加载。更糟糕的是,如果应用程序访问了一个只存在某个版本类库的类,那么该类也会被加载。这就意味着调用该类库的程序会混合着使用这两个版本类库的代码。

正由于应用程序需要访问同一类库的不同版本,因此当其中一个版本不存在的时候,应用程序极有可能无法正常工作。要么是程序行为不符合期望,要么就抛出NoClassDefFoundErrors异常。

复杂的类加载
默认情况下应用程序的所有的类都是由同一个类加载器(ClassLoader)进行加载,但是开发者也可以自由地添加额外的类加载器。

这通常是由类似组件系统和网络服务器这类容器(Container)实现的。理想的情况下,这种隐式的使用(额外的类加载器)对开发者是透明的。但是,众所周知,所有的抽象都不是十全十美的。在某些特殊情况下,开发者可能要显式地使用额外的类加载器来实现一些功能。例如允许用户可以通过加载新的类来扩展程序的功能,或者让用户可以使用同一个依赖的相互冲突的版本。

暂且不管该问题是如何造成的,使用多个类加载器会很快使得类加载机制变得异常复杂,从而导致难以预料且难以理解的行为。

Classpath地狱和依赖地狱
虽然JAR地狱似乎更多地关注于由复杂的类加载器层次结构造成的问题,但从本质上讲,Classpath地狱和JAR地狱确是同一个东西。两者都只会出现在Java和Java虚拟机。

相对而言,依赖地狱(Dependency hell)使用得更为广泛。依赖地狱描述的是软件包和依赖之间的一般性问题。它既存在于操作系统中,也存在于个人开发的系统中。正由于其普遍性,它并不包括那些只存在于特定某个系统的问题。

上面列举的5大问题里,依赖地狱覆盖了依赖往往难以言明、依赖具有传递性以及版本冲突这3大问题,但不包括类加载和屏蔽问题,因为它们Java里特有的机制。

Published by the Wellcome Library under CC-BY 4.0Published by the Wellcome Library under CC-BY 4.0

行业现状
构建工具
回顾上述列举的问题,构建工具有助于解决其中部分问题。由于具有传递性,依赖关系最终会形成一棵具有无数条边的依赖树。而构建工具在声明依赖这方面非常拿手,这样它们就能够沿着依赖树的边找到所需的JAR。这很大程度上解决了依赖难以言明和具有传递性问题。

但是Maven等工具对于 屏蔽问题却无能为力。虽然它们一直努力试图减少重复的类文件,却无法避免该问题。而对于版本冲突问题,构建工具能做的只是识别出这些冲突的类。它们更是无法干涉类加载问题,因为这只发生在运行时。

组件系统
对于OSGi和Wildfly这样的组件系统,我从来没有使用过,因此无法评价它们的作用。从它们的介绍来看,它们似乎能解决大多数JAR地狱问题。

使用组件系统必将使得问题更加复杂。同时,这往往需要开发者对类加载机制有更加深入的了解。讽刺的是,复杂的类加载机制正是上面列举的问题之一。

不管组件系统是否真的能够解决JAR地狱问题,至少在我的印象中,绝大部分的项目里都没有用到它们。也就是说,绝大部分的项目依旧深陷于classpath相关问题的泥潭中。

还有哪些问题亟待解决?
由于使用范围不广,组件系统能够覆盖的领域还是相当有限。相反,普及率相当高的构建工具对于解决JAR地狱作用显著。

在我参与过或者听说过的项目里,如果没有不使用构建工具的话,那么开发者将要花费相当多的时间用来解决依赖难以言明和具有传递性问题。同时,屏蔽问题也时不时漏出其丑恶的面目,想要解决它也要耗费不少时间。

而对于版本冲突问题,每个项目迟早都会遇到,并且不得不做一些艰难的决定才能解决它。通常,一些急切的功能升级不得不往后推迟,就是因为它依赖于其他的升级,而这些升级在现阶段无法进行。

对于大部分具有一定规模的应用、服务和类库,我敢说,版本冲突问题是影响何时及如何升级依赖的主要因素之一。这是我无法忍受的。

对于繁琐的类加载机制,我所知甚少,因此很难评判该问题出现的频率。就目前我所参与的项目来看,没有任何一个项目需要用到繁琐的类加载机制。据此可以看出该机制并不常用。在网上搜索何时要用到该机制时,结果都指向我们前面讨论过版本冲突问题。

据我自身的经验来看,版本冲突是JAR地狱所引发的最严重的问题。

版本冲突是JAR地狱所引发的最严重的问题。

总结
本文探讨了构成JAR地狱的主要方面:

依赖往往难以言明
依赖具有传递性
屏蔽
版本冲突
复杂的类加载
归功于构建工具和组件系统的作用,以及它们在项目中被广泛使用,我们可以得出以下结论:依赖难以言明和具有传递性问题很大程度上得以解决,屏蔽问题也得到一定的缓解,复杂的类加载机制并不常用。

因此, JAR地狱所引发的最严重的问题就是版本冲突,这问题会一直影响大多数项目的升级和更新。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值