Maven依赖冲突问题原理简析

Maven在jar包冲突的情况下,很容易出现NoSuchMethodError,NoClassDefFoundError等问题。遇到的多了自然知道这是依赖冲突产生的问题,但是为什么会产生这种问题?其原理是什么?细细思考下,结合maven编译,打包以及jvm类加载机制总算将来龙去脉想的差不多了,今天这篇博客就把我关于这个问题的思考总结写写。

先来看一个maven依赖解调的图:
maven依赖图

maven会对项目内的jar包依赖做调解,确保同一个依赖(相同groupId,artifactId)在项目中只引入一个版本。依赖解调遵循两个原则,路径最近原则以及定义顺序原则,根据以上原则,上图中maven解调后的各jar的版本如图右侧所示。jar A的依赖都被B的替换了,这会发生什么样的事情?在说这个前我们先讲讲maven对jar的打包机制。

大部分IDE在运行前都需要先编译,编译过后才能打包,然后运行,这是Java的一种自我保护机制,能通过编译,就说明我们项目内的代码之间的引用是合理的,通过校验的,这是运行的基本保证。通过编译后,如果我们的项目打包形式是jar,那么就可以将项目打包成jar包,给其他人使用。但是这个jar包里只有我们自己的代码,并不包含我们项目的第三方依赖。

对比上图,项目X在打包成功为jar X后,jar包里只有X项目的代码,不包含jar A,B,C,D,E 的代码。但是jar X的pom文件里有dependency元素A,B,C,IDE在引入jar X后会解析其pom文件,引入 jar A,B,C,递归解析,再引入jar D,E。

项目的编译仅仅是编译当前项目内的代码,打包也仅仅是打包当前项目的代码。编译后的jar/war能够正常运行的前提是编译时的环境(依赖)不变或者能被兼容。

像上图,jar A 1.0能够正常运行的前提是jar C ,jar D ,jar E 这三个jar包里有jar A 1.0运行时需要的东西。但jar X引入jar A后,经过maven依赖解调,其原本的依赖版本都被替换了,这可能会发生什么?是什么原因?我们从jar包版本间可能存在的差异说起。

jar版本差异

如上图,不同的jar版本之间可能存在的差异:

  1. 类数目有变化,可能增加或者减少
  2. 类结构有变化,可能增加/减少 Field,Method。
  3. 成员有变化,可能Field类型改变,可能Method参数,返回值有变化,可能修饰符改变
  4. 配置文件可能有变化
    等等。

版本兼容,指的是后面的版本兼容之前的版本,也就是说后面版本的东西相对于之前版本来说,开放元素只会增加不会减少。不会修改public,protected的Field,Method的定义(定义,相当于外壳,Method内部的实现是可以修改的),不会移除public,protected的Class(基于如此原理,选择合适的Field,Method,Class的作用域就相当关键)。也就是用户更新了jar的版本后,在运行时至少能达到之前版本一样的效果,同时新加的内容能够有额外的功能或者比之前的实现得更好。

项目在运行中(启动过程也算运行),当第一次接触某个类时(使用new生成此类对象,调用其static方法,获取其static属性,使用反射获取类相关信息等),会先加载这个类。遵循双亲委派模式,尽量保证一个类只被加载一次,应用里类不会重复。

一个类在jvm里的生命周期有以下几个步骤:

  1. 加载 :以二进制流的形式加载class文件到jvm中,在方法区中定义Class对象以应对外部程序的访问
  2. 验证 :验证class的文件及数据结构是否符合规范,
  3. 准备 :为类变量分配内存并设置类变量初始值
  4. 解析 : 将类对其他类的引用从字符形式替换为内存地址的形式
  5. 初始化 : 按类的层次结构由上往下依次执行器静态代码块内的程序
  6. 使用
  7. 卸载

NoClassDefFoundError错误主要发生在第一个阶段:加载。当使用到一个类时,发现jvm内没有这个类的数据,那么第一步要做的就是加载这个类,加载的源头很多,从ZIP包处,比如JAR,EAR,WAR等,应用最多的自然是用jar包里加载了;也可以从网络获取,比如Applet;或者运行时产生,最典型的就是JDK Proxy和CGLIB 动态代理了;也可以由其他文件生成,比如由JSP生成相应的Class类等等。这里讲的加载其实就是将二进制文件以流的形式读入jvm中,然后在方法区中实例化一个Class< T>的对象来作为外部程序访问此类数据的入口。

在项目运行时,通过调用ClassLoader.loadClass(String name)方法,也可以加载类数据,此方法抛出一个ClassNotFoundException,需要开发者捕获加以处理,这种异常jvm认为不是致命的,可以加以处理而不影响程序运行。而经过编译打包形式后的类加载出现问题,比如上图,jar C 1.1里某个类CC移除了,而jar A 1.0的类AA的init()方法里使用到这个类,那么在项目运行过程中,如果调用AA的init( )方法,此时发现类CC不在jvm中,此时jvm从classpath里去加载这个类,但是发现其不存在,那么就会抛出NoClassDefFoundError,这是个运行时的致命错误,项目异常终止。

NoSuchMethodError发生在使用阶段,线程的运行实质上是想虚拟机栈中一个一个的压入Method的过程,执行一个方法,push一个Method,执行完此Method,pop此Method。当因为jar包版本改动而导致原先的方法不存在了或者方法签名改变(参数,返回值,修饰符等变化),在线程运行时,通过原有的方法签名去寻找相应的Method时,找不到了,jvm抛出NoSuchMethodError。而通过反射形式获取不存在的Method,jvm抛出NoSuchMethodException。

通过了解依赖冲突问题的原理能更好的理解排查及解决jar包版本冲突问题。发现版本冲突处后可以在pom依赖元素中使用< excludes> 形式排除间接的依赖。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值