ITPub博客

首页 > 数据库 > 数据库开发技术 > 一次硬核的项目jar包冲突实战

一次硬核的项目jar包冲突实战

原创 数据库开发技术 作者:安瑞哥是码农 时间:2022-05-13 19:41:39 0 删除 编辑

前言
前几周因为做了spark的大版本升级,导致原本的软件项目中的代码有多处改动,因为计算引擎位于软件项目的核心位置,上承数据源,下接数据库,所以要改动计算引擎,势必会引起诸多的连锁反应。

在前面的文章中都已经聊过了很多关于spark2.x如何升级spark3.x,以及中间会经历哪些坑,可以说已经解决了升级过程中90%的问题。

但是因为项目庞大,我现在的软件工程中有大大小小10多个子工程(也叫子module),且彼此之间存在各种依赖关系,所以项目引入的jar包会非常的多,我大概数了下得有500个左右jar包依赖。

如此庞大的jar包数量,最容易出现的一个问题就是jar包冲突。

相信很多大数据开发的小伙伴都遇到过,当然如果你说你没有,那只能说明还没有到时间而已,或者你根本不知道当前导致程序报错的原因是由jar包冲突导致的,然后一头雾水,求助各种Google和百度,最终被网络上那些没有前因和后果的文章绕的更加晕头转向。

其实,简单的jar包冲突问题很好定位,但是有一些难的,隐藏地比较深的,你很难根据常规经验来判断。

今天这篇文章,就来通过我们项目中实在发生的,一个隐藏的很深的jar包冲突问题,来告诉你,如何分辨,如何定位,以及如何解决该类问题。


什么是jar包冲突

在开始之前,我们先来看下的为什么会有所谓的jar包冲突问题产生?


我们知道,对于大数据项目来说,你的代码为了满足各种对数据处理的要求,maven依赖中一定会引入非常多的对应功能jar包,而不同的jar包中,还会同时存在很多的子依赖,而在各个子依赖中,可能同时引用了同一个名称(包名相同)但是不同版本的jar包(当然还会存在不同的jar包之间,有着一模一样类名的时候,但是这个情况不多)。


来看下面这个图:

一个软件项目的jar包依赖示意图

该项目同时依赖了包A、B、C,而它们彼此又有很多子依赖,在众多子依赖中,它们同时依赖了一个叫F的jar包,但是彼此之间的版本各不相同,而如果我的项目想要正常运行,只能加载F的2.5版本,但是你无法保证每次程序运行时,其类加载的顺序是按照你的要求来,假如它加载了F的1.8版本,而这个版本的某个类跟2.5的版本相比有较大的改动,比如缺少某个功能,或者没有某个关键函数,就会导致程序运行报错。


而这个时候的报错行为,我们就称之为jar包冲突。


先看个简单的例子

了解了jar包冲突的原理后,我们先来个开胃菜,举个简单的jar包冲突例子。


来,先看这个最简单的spark程序。

import org.apache.spark.{SparkConf, SparkContext}
object Demo {
    def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf()
        sparkConf.setAppName("jar包冲突测试")
        sparkConf.setMaster("local")
        val sparkContext = new SparkContext(sparkConf)
        val seq = Seq("a","b","c","d")
        val rdd1 = sparkContext.parallelize(seq)
        rdd1.foreach(data => println(data))
    }
}


再瞅一眼其maven依赖情况:

    <dependencies>
        <!--提供基础的scala运行环境,类似Java的JDK-->
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>2.11.7</version>
        </dependency>
        <!--提供SparkConf和SparkContext对象-->
        <dependency>
            <groupId>org.apache.spark</groupId>
            <artifactId>spark-core_2.12</artifactId>
            <version>3.2.0</version>
        </dependency>
    </dependencies>

好,来运行一下看看,会不会出什么幺蛾子:

jar包冲突导致的报错
果然,报错了,我们来看下这个保的错,说scala.Product这个类没有找到其构造方法(init方法)。

那我们就来看下这个类属于哪个jar包,通过IEDA快捷(Ctrl+Shift+Alt+N)
来查找:

找到该类对应所属的jar包
可以看到该类属于scala-library,且版本为2.11.7,既然有这个类却无法初始化,那我们下一步就来看下,是不是加载错了jar包,因为正常情况下一定是可以初始化的,对不对?

好,现在就来看下当前maven的依赖环境里是不是有多个scala-library版本,通过IEDA的工具,查看该maven依赖的依赖树,如下图:

依赖树中存在2个不同的版本
可以看到,一个版本为2.12.15,而另一个为2.11.7,版本差异还比较大,要知道,2.12跟2.11有很多地方都做了改变,那我们用哪个呢?

判断依据就是:看我们现在的主体包spark-core支持哪个版本,我们就用哪个版本,其实你只要稍微留心一点就会发现,这个spark-core包的包名本身就告诉你了scala-library的版本,上面显示spark-core_2.12,其中这个2.12就是它支持的scala版本号。

那么这个scala-library的2.12.15版本哪来的呢?我没有单独引入啊,那就只有一种可能,它存在于spark-core的子依赖中。

来瞅一眼,用IDEA打开spark-core的pom文件:

查看spark-core的子依赖
来瞅一眼,可以赫然看到其中有scala-library的依赖,所以我们上面再引入一个2.11.7的版本就多此一举,而且带来了jar包冲突问题。

解决办法,也特别简单,把2.11.7这个多余的依赖去掉,就解决了。

再来看个升级版的
如果所有的jar包冲突问题都如同上面一样简单的话,我就不会写这篇文章了。

现在,我们再来看个升级版的jar包冲突问题,这个时候我切换到另一个复杂的软件项目中,否则没有办法演示出这个效果。

先大概说下这个项目的结构,目前这个项目有10多个子module,其中多个module之间都存在依赖关系,现在我就拿依赖最重的那个module来说明。

我在这个module中,为了方便debug,还是用刚才那个最简单的spark程序。
object BigdataDemo {
    def main(args: Array[String]): Unit = {
        val sparkConf = new SparkConf()
        sparkConf.setAppName("jar包冲突测试")
        sparkConf.setMaster("local")
        val sparkContext = new SparkContext(sparkConf)
        val seq = Seq(1,2,3)
        val rdd1 = sparkContext.parallelize(seq)
        rdd1.foreach(println(_))
    }
}
代码贼简单,不解释,直接运行,看下运行效果:

可以看到报错了,提示NoSuchMethodError,一股浓浓的jar包冲突的味道扑面而来。

以我的经验,凡是报错说NoSuchMethodError的几乎都是jar包冲突导致。

既然告诉我类io.netty.buffer.PooledByteBufAllocator没有某个方法,那我们就来看下哪个或者哪几个jar包中含有这个类。


来,还是通过IDEA的快捷(Ctrl+Shift+Alt+N),看下查找效果:

可以看到,出现了2个版本的netty-all,说明我们当前的软件工程中是存在两个不同版本的netty-all的,那我们来进一步验证一下,当前这个module中,其依赖的所有jar包中,是否也含有netty-all的版本?


怎么看?通过IDEA的工具,打开当前module中maven依赖的依赖树,查找当前module中依赖的netty-all的版本,具体方式这里不赘述:

可以看到,我能找到2个不同版本的netty-all,其中一个是4.1.17另一个为4.1.68,这里我只能留一个,留哪个?


那是那句话,得根据项目的主体来定,我们这个项目的主体是谁?是spark对不对,一个最简单的方法,就是打开服务器spark的home目录,然后查看里面的lib目录下的jar包情况,找到其中netty-all的jar包,跟其保持一致,就错不了。


打开之后可以看到,是这样的:

赫然显示4.1.68版本,因此,果断将原来的4.1.17给排除掉,具体排除方式这里也不赘述。


排除后发现只剩下一个netty-all的版本了:

我们再来运行下代码试试:

啊哦... 还是报错了,但是这次错不一样了,提示居然说数组下标越界?


纳尼? 我这代码中没有对数组进行下标操作啊,怎么就下标越界了呢?


如果你一直把问题纠结在代码处理的数据上,就正好就中了这个问题给你设下的圈套。


首先要非常明确的知道,这个肯定不是我代码处理数据的问题,但是在代码的一层层调用过程中,当调用到某个类的某个方法时,才抛出的这样的异常。


那我们是不是可以来看下这个抛出异常的类,叫BytecodeReadingParanamer.java,我们把他打开来看看:

可以看到,这个类属于一个叫paranamer-2.3的jar包,按理说这样一个不怎么起眼的包,正常情况下是不会有问题的,难道是调用的版本不对吗?


验证一下,还是用刚才的办法,在当前module的依赖树中找出这个包:

果然,发现有2个不同的版本,一个2.3另一个2.8,我们排除一个试试,排除哪个?

还是用刚才那招,看spark家目录中对应的这个包是哪个版本:

确定是留2.8,那么就把2.3的版本给排除掉:

排除之后,我们再运行下程序试试:

我还沉浸在解决刚才那个冲突问题的喜悦中呢,又报错了,又是一个NoSuchMethodError,又一个典型的jar包冲突问题。


还是老方法,先查看该类对应的jar包:

可以看到好几个不同的jar包版本,留哪个?老规矩,看spark家目录中对应的jar包版本,把低版本的给排除掉。

那我们就只留1.7.30,其他版本的给排除掉(具体方法不赘述),再试试:

终于,经过了3次jar包的版本调整,总算运行成功了,就为了这3个数字,容易嘛我。


得说明一下,这个是发生在我项目中的真实案例,并不是为了攒出这篇文章而故意营造的代码事故。


而且更值得说明的是,在我解决这个jar包冲突问题之前,代码虽然无法在本地IDEA中运行,但是如果打包提交到yarn集群上,却一点事也没有,进一步增加了jar包冲突问题的不可预料性。



结语

我之前还遇到过更棘手的情况,就是两个不同名称的jar包,出现了同一个类名,但实现却不相同,而且这个两个jar包在项目运行中缺一不可,当时的解决办法是将有冲突的那个类进行重命名,然后让程序只加载需要的那个类,具体实现,以后有机会再讲。


对于jar包冲突问题的解决,最难的点在于对问题本身的识别,你要能知道当下这个报错就是因为jar包冲突导致,而不是别的原因。


识别到了,解决起来就简单了。


对于jar包冲突的案例,以上的场景基本上能覆盖到你的大数据开发项目中的90%以上。


以上内容,希望对你有所帮助。



我这有一本spark学习的书,内容深入浅出,如果你有兴趣,加我送你啊。另外,如果你有任何技术问题,也欢迎找我交流探讨...


原文作者:安瑞哥是码农;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2022-02-16

  • 博文量
    228
  • 访问量
    115449