Java依赖冲突高效解决之道(1)

发表于:2022-2-07 09:15

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:阿里技术    来源:网络

  一、概述
  由于阿里妈妈联盟团队负责业务的特殊性,系统有庞大的对外依赖,依赖集团六七十个团队服务及N多工具组件,通过此文和大家分享一下我们积累的一些复杂依赖有效治理的经验,除了简单技术技巧的总结外,也会探讨一些关于这方面架构的思考,希望此文能系统彻底的解决java依赖冲突对大家的困扰。
  二、依赖冲突产生的本质原因
  要解决依赖冲突,首先要理解一下java依赖冲突产生的本质原因。
图1

  以上图为例,目前阿里大部分Java工程都是maven工程,此类工程从开发到上线要经历以下两个重要步骤:
  1.编译打包
  平时我们编写的应用代码,用maven编译应用代码时,maven只依赖第一级jar包(A.jar,B.jar,*.jar)既完成应用代码的编译,至于传递依赖的jar包(Y.jar,Z.jar)maven首先会对同名不同version的jar包进行依赖仲裁,然后依据仲裁结果下载对应的jar放到指定目录下(例如上图中Y.jar最终只会仲裁1.0或2.0一个版本,此处假定仲裁到2.0版本,Z.jar即便内容与Y.jar一致,但名称不一样所以不属于maven仲裁范畴)。
  有一点需注意不同maven版本可能会有差异,这会导致有时本地环境和日常、预发打包不一致造成应用逻辑表现不一致的情况(说明一下这种情况还有其他一些原因会导致,不是说一定是maven版本不一致仲裁结果不一致导致的)。
  2.发布上线
  先明确一个概念,在JVM中,一个类型实例是通过它的全类名和加载它的类加载器(ClassLoader)实例来唯一确定的。所以所谓的“类隔离”,实际就是通过不同的类加载器实例去加载需要隔离的类来实现的,这样即便两个全类名完全相同但内容不同的类,只要他们的类加载器实例不同,就能在一个容器进程中共存,并且各自运行互不干扰。
  发布启动容器时,不管是tomcat、taobao-tomcat还是PandoraBoot,还是其他容器, 首先都是用特定的类加载器实例先加载容器本身依赖的jar包,容器一般都会有多个类加载器实例,容器自身所依赖的jar包一般由专门的类加载器实例加载实现与应用包的绝对隔离,像Pandroa还有专门的类加载器实例加载淘系中间件避免中间件与应用类冲突,如下图所示:
  容器内部依赖jar加载完成后,才轮到必然的一步:由某个应用ClassLoader实例(一般与容器类加载器实例不是一个)来加载编译打包阶段打出来的应用jar包及应用.class程序,这样容器才能运行业务,同时确保应用不会干扰容器的运行。
  例如图1中,最终打出的应用包中Y.jar-2.0,Z.jar都有com.taobao.Cc.class类,但一个应用ClassLoader实例仅能加载V3或V2中一个版本的com.taobao.Cc.class类。
  那到底会加载哪个版本的com.taobao.Cc.class类呢?答案是不一定,这个取决于容器应用类加载实现策略, 从以往遇到的情况看,tomcat,taobao-tomcat、Pandora的做法都是直接装载应用lib包下所有.jar包文件列表(上例是A.jar,B.jar,*.jar,Y.jar,Z.jar。除tomcat外都没看源码核实过,有错欢迎纠正)。但Java 在装载一个目录下所有jar包时, 它加载的顺序完全取决于操作系统!而Linux的顺序完全取决于INode的顺序,INode的顺序不完全能一致,所以笔者之前就遇到类似的问题,上线20台机器,用同一个镜像,有2台就是起不来的情况。遇到这种情况目前就只能乖乖按以下章节中的手段去解决了。理论上最正确的做法应该是容器装载应用 jar包时,按指定顺序加载。
  基于以上分析,我们可以得出结论,基本所有的类冲突产生的本质原因:要么是因为maven依赖仲裁jar包不满足运行时需要,要么是容器类加载过程中加载的类不满足运行时需要导致的。
  关于容器类加载隔离策略,网上ATA上有很多资料介绍,本文重点向大家讲解遇到冲突的各种解决之道,解决冲突大家只需要知道以上重点原理就够了。
  理解了依赖冲突产生的本质原因,那么发生依赖冲突如何高效定位具体是哪些jar包引起的冲突呢?请继续看下一章节。
  三、依赖冲突问题高效定位技巧
  发生依赖冲突主要表现为系统启动或运行中会发生异常,99%表现为三种NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError。下面逐一讲解一下定位技巧。
  1.NoClassDefFoundError、ClassNotFoundException排查定位步骤
  STEP1、发生NoClassDefFoundError首先要看完整异常栈,确认是否是静态代码块发生异常,静态代码块发生异常堆栈与jar包冲突有很明显的区别,出现"Could not initialize"、"Caused by: ..."关键字一般是静态代码块发生异常导致类加载失败:
  java.lang.NoClassDefFoundError: Could not initialize class testing.User  
      at testing.Test.main(Test.java:23) 
  Caused by: java.lang.RuntimeException: UserId Not found 
      at testing.User.getUserId(Test.java:41) 
      at testing.User.<clinit>(Test.java:35) 
      ... 1 more 

  因为静态代码块发生异常导致NoClassDefFoundError,修改静态代码块避免抛出异常即可。如果不是静态代码块发生异常导致的问题,继续下一步。
  STEP2、如果不是静态代码块发生异常导致加载失败,异常message关键字中会明确显示缺失的类名称,例如:
  java.lang.NoClassDefFoundError: org/apache/commons/lang/CharUtils 
      at testing.Test.main(Test.java:19) 

  STEP3、在IDEA中(快捷键Ctrl+N)查找异常栈中提示缺失的类在哪些版本的jar包中有,如上例中的org.apache.commons.lang.CharUtils。
  STEP4、查看应用部署机器上应用lib包目录下(一般是/home/admin/union-uc/target/${projectName}/lib或union-pub/target/${projectName}.war/WEB-INF/lib)是否存在上一步骤中查出对应版本的jar包,以上情况一般是因为此时应用依赖的是低版本jar包,而jar包中又没有冲突的类,绝大部分情况下NoClassDefFoundError、ClassNotFoundException定位确认都是因为maven依赖仲裁最终采纳的jar包版本与运行时需要的不一致导致。
  2.NoSuchMethodError排查到位步骤
  STEP1、发生NoSuchMethodError,异常堆栈日志核心片段(异常栈中处于栈底的片段,见过很多同学发生异常乱翻一通,那样毫无意义,要有目的的翻关键地方,不要乱翻)会明确显示具体是哪个类,缺失了哪个方法,异常堆栈核心片段示例如下:
  Caused by: java.lang.NoSuchMethodError: org.springframework.beans.factory.support.DefaultListableBeanFactory.getDependencyComparator()Ljava/util/Comparator; 
      at org.springframework.context.annotation.AnnotationConfigUtils.registerAnnotationConfigProcessors(AnnotationConfigUtils.java:190) 
      at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.registerComponents(ComponentScanBeanDefinitionParser.java:150) 
      at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:86) 
      at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73) 

  首先需确认JVM中当前加载的缺失方法类,如上"org.springframework.beans.factory.support.DefaultListableBeanFactory"类到底来自哪个jar包,目前最高效的办法:
  外部环境容器下,或者某些容器版本过低不支持Arthas在线诊断的情况下,可以通过在JVM启动参数中增加" -XX:+TraceClassLoading",然后重新启动系统,在系统工程日志中即可看到JVM加载类的信息。从中即可找到JVM是从哪个jar包中加载的。
  STEP2、在IDEA中(快捷键Ctrl+N)查找异常栈中提示缺失的类在哪些版本的jar包中有,如下图所示:
  然后依次查看各版本jar包中冲突类的源码,工程中部分jar打包时附带了源码包可直接看到源码,不带源码的需要用IDEA插件(推荐jad)反编译一下。然后依次搜寻各个jar包中的冲突类,搜寻第一步是点击上图中某个版本类,在IDEA中查找类级次关系(快捷键Ctrl+H),如下图所示:
  然后在冲突类及所有冲突类的父类源码中找到NoSuchMethodError异常信息中描述缺失的方法,以上例子中就是"getDependencyComparator()Ljava/util/Comparator"。
  上例中通过搜寻可以发现spring-beans-3.2.1.RELEASE.jar,spring-2.5.6.SEC03.jar两个版本DefaultListableBeanFactory类及父类中没有"getDependencyComparator()Ljava/util/Comparator"方法,spring-beans-4.2.4.RELEASE.jar,spring-beans-4.3.5.RELEASE.jar两个版本DefaultListableBeanFactory类中有缺失的"getDependencyComparator()Ljava/util/Comparator"方法。
  STEP3、查看应用部署机器上应用lib包目录下(一般是/home/admin/union-uc/target/${projectName}/lib或union-pub/target/${projectName}.war/WEB-INF/lib)下,找到相关jar包的版本,如上例中:
  致此定位问题根本原因是应用启动时加载"org.springframework.beans.factory.support.DefaultListableBeanFactory"类未加载到运行时预期所需的spring-beans-4.3.5.RELEASE.jar版本,而是加载了spring-2.5.6.SEC03.jar导致。
  按照以上流程步骤,基本99%的依赖冲突都可以定位到根本原因。定位到原因后如何解决冲突呢?事实上有些时候解决冲突远没有内网上很多帖子描述的"mvn dependency:tree"一下,排排jar那么简单。具体细节请继续看下一章节。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号