Cglib引发的血案——Jar包依赖冲突总结

上周工程引入新的依赖时,出现了CGLIB包冲突。所以想总结一下包冲突的问题。这是一个老生常谈的话题,是人人必会遇到的。尤其是随着工程规模越大,外部依赖越多,发生的概率就越大。消除依赖冲突是每个Coder必备技能之一,所以一起来总结一些常见的问题及解决方式,否则摸不到头绪的时候,想消除依赖冲突是一件能让你抓狂的事情!


什么是依赖冲突

貌似没有哪里有官方的解释。按照我们自己的理解就是类加载时,找不到正确的类或者使用了错误的类。细分下来有2种场景:

  1. 工程内部同时引用了同一包,但不同版本的多个依赖。此种场景下,类加载时,有可能选择到了错误的版本
  2. 同样的类(包名类名完全相同,这个概念要清楚。并不是指包括方法签名都一样的类,仅仅指包名和类名)存在于多个不同的依赖包中。此种场景下,同样的类会有多个版本,类加载时,有可能选择到了错误的版本

上述2种情况仅是发生场景的不同,其结果都是一致的,都会导致类加载器加载不到正确的类,进而发生各类问题。接下来根据不同的场景详细分析。要解决问题,首先要分析问题发生的根源。也就是在什么样的前置条件下,会导致这个问题发生。接着再针对每一种可能出现的前置条件一一处理。

场景一:同一Jar包出现了不同版本

  • 前置条件
    • 依赖传递导致——简单说就是多个不同的依赖包间接依赖了同一个包的不同版本
    • Maven的版本仲裁选择了错误的版本
    • 不同版本之间的类或接口存在差异,且当前工程中有依赖了其中有差异的方法

场景二:同一个类出现在不同的Jar包中

  • 前置条件
    • 假设同一个类A存在于X包和Y包中,类加载器如果想要的是X包中的A,而加载时先选择加载了Y包中的A,那就是加载到了非期望的版本

冲突的原因

Maven仲裁机制

在当前Maven的天下,场景一无法避免不谈到Maven仲裁机制。因为Maven具有的传递性,通常简化了我们引用Jar包的管理,但给Maven自身带来了复杂性。于是Maven出了一套仲裁机制自行管理依赖版本。但Maven选择的版本很有可能不是我们所期望的。

讲到这里,有的同学对Maven仲裁机制不是很了解,那就先温习一下:

  1. 优先使用依赖管理标签(dependencyManagement)内定义的版本,此时其他声明均无效
  2. 若没有声明版本,按照最短路径进行仲裁
  3. 若路径一样长,按照最先声明进行仲裁

按照序号排列顺序仲裁。

依赖包加载顺序

假如一个工程内既有Jar A的V1版本,也有它的V2版本。在类加载时能固定一个顺序,岂不是就可以规避冲突了?理是这么个理,可是实现起来比较麻烦了,为什么这么说?

  • Java的类加载机制是面试热点,一起复习一下。JVM类加载机制使用的是双亲委派模式——加载顺序由高至低,引导(启动)类加载器->扩展类加载器->系统(应用)类加载器->自定义类加载器,每个加载器对应加载不同的路径下的包。
    • 引导类加载器——JAVA_HOME/jar/lib/rt.jar目录下的核心类或-Xbootclasspath指定目录下的类
    • 扩展类加载器——JAVA_HOME/lib/ext目录下的扩展类或-Djava.ext.dirs指定目录下的类
    • 系统类加载器——-CLASSPATH或-Djava.class.path所指的目录下的类(一般就是我们所开发的Java应用下的类)
    • 自定义类加载器——想加载哪就加载哪,自定义类继承ClassLoader,重写方法
  • 系统的文件加载顺序,最诡异的冲突制造者。比如Tomcat的类加载器获取加载路径下的文件时不排序,此时完全依赖系统返回的文件顺序。在开发环境、日常环境和测试环境就是没问题,一上线就出问题了。简直让人抓狂,这个时候你可能就要关注一下非线上的正常运行环境与线上是否一模一样了。

冲突的结果

冲突后的结果无非两类,异常或者结果与预期不一致。

异常 起因
ClassNotFoundException 通常由于仲裁到了错误的版本,运行期没有找到我们所需要的类。
NoSuchMethodError 2类冲突都有可能
NoClassDefFoundError 雷同
LinkageError 雷同

除了表格里这些异常,还有其他异常。不做一一分析了,大致都是上文阐述的两类冲突。

解决问题

要解决先要排查

  1. 通过异常堆栈信息找到冲突的类名
  2. 利用开发工具定位到该类存在于哪个包中
  3. 使用Maven命令打印冲突树mvn dependency:tree -Dverbose -Dincludes=<groupId>:<artifactId>,确定到冲突包所在位置
  4. 根据冲突场景选择不同处理方式。
冲突场景 解决方案
同一Jar包出现了不同版本 excludes排除或声明一个版本(推荐使用后者)
同一个类出现在不同的Jar包中 优先排除,若不能排除,就升级或者寻找可替换的包

除了常用的解决方式以外,还可以利用类加载机制实现Jar包隔离。比如目前我司内部使用的Tomcat容器是定制包装的,针对某些易冲突的依赖,都会呼吁相关部门开发所谓的“插件”,其本质就是将依赖以“插件”形式扔进容器中,实现依赖隔离,交给容器来加载,从而避免冲突。当然,我们也可以自定义一个类加载器,只不过这么做看起来并不是最合适的方案,自定义类加载器实现隔离应该放在解决依赖冲突的所有方案中的最后选择。

有备无患

如果第二类冲突发生,且又不容易直接排除时,只能选择升级Jar包或者替换时,问题就变得相当复杂,非常耗时。所以最好提前预防。建议:

  1. 在父pom依赖管理标签中进行统一版本管理,子pom不再声明版本,直接引用
  2. 不要将无用依赖引入,废弃依赖及时从pom中删除
  3. 善用插件maven-enforcer-plugin + extra-enforcer-rules工具,能自动扫描Jar包将冲突检测并打印出来

案例

同一Jar包出现了不同版本

最常见的冲突,但也是最好解决的。上文已经说过,此处不再重复。见 解决问题

同一个类出现在不同的Jar包中

回到本文开头部分提到的CGLIB血案。A包中使用了cglib-nodep 3.4,B包中间接依赖了asm 2.2,C包中间接依赖了cglib 2.2,D包中使用了cglib-nodep 2.2。

排错解决的过程就不多说了,总之是很“艰难”,最终的解决方案是:

  1. 排除所有cglib
  2. 排除所有asm
  3. 仲裁声明cglib-nodep 2.2版本

总结一个关于cglib相关包冲突的使用心得吧~

  1. cglib-nodep不可与cglib、asm共存,若要使用cglib-nodep,需要排除掉所有的cglib和asm。这是因为cglib-nodep = cglib + asm
  2. cglib搭配asm使用
  3. cglib与asm的版本号搭配上,每个小版本之间有可能存在差异,所以只能通过试错总结版本搭配
  4. cglib-nodep 3.1版本是风水岭,3.1以下(包括3.1)中asm相关类的包名和类名与3.1以上版本有差异

另外之前也遇到过Apache的commons-lang包,2.x升级到3.x时,包名从commons-lang变为commons-lang3,部分接口也不同,由于包名不同和传递性依赖,经常会出现两种Jar包同时存在。org.apache.commons.lang.StringUtils.isBlank方法就是一个典型,如果加载了错误版本,就会出现NoSuchMethodError异常。

白十 wechat
欢迎订阅我的微信公众号!