当前位置: 首页 > 后端技术 > Java

Java依赖冲突高效解决

时间:2023-04-01 15:11:57 Java

介绍:由于阿里妈妈联盟团队业务职责的特殊性,系统存在巨大的外部依赖,依赖于集团六七十个团队服务和N个多工具组件,通过本文分享下面和大家一起来看看我们在有效治理复杂依赖方面积累的一些经验。除了简单的技术技巧总结外,我们还会讨论一些关于这方面架构的思考。希望本文能系统、彻底地解决Java依赖冲突带来的问题。作者|澄江源|阿里巴巴科技公众号1概述由于阿里妈妈联盟团队业务的特殊性,系统对外依赖性巨大,依赖于集团六七十个团队服务和N个多工具组件,通过本文分享给大家你不妨看看我们在有效治理复杂依赖方面积累的一些经验。除了简单的技术技巧总结外,我们还会讨论一些关于这方面架构的思考。希望本文能系统、彻底地解决Java依赖冲突带来的问题。二依赖冲突的本质原因要解决依赖冲突,首先要了解java依赖冲突的本质原因。上面的图1是一个例子。目前阿里的Java项目大部分都是Maven项目。此类项目从开发到上线,要经过以下两个重要步骤:1.将我们平时编写的应用代码进行编译打包。在使用Maven编译应用代码时,Maven只依赖一级jar包(A.jar、B.jar、*.jar)来完成应用代码的编译。对于传递依赖的jar包(Y.jar、Z.jar),maven会先使用同名不同版本的jar包进行依赖仲裁,然后根据仲裁结果下载对应的jar放到指定的目录中目录(比如Y。内容和Y.jar一致,只是名字不同,所以不属于maven仲裁的范畴)。需要注意的一点是,不同的maven版本之间可能存在差异,有时会因为本地环境与daily和pre-release打包不一致导致应用逻辑性能不一致(说明这种情况还有其他原因,不一定不一致的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的顺序完全取决于INodes的顺序,而INodes的顺序并不完全一致,所以笔者之前也遇到过类似的问题。当线上有20台机器时,使用同一个镜像,有2台机器无法启动。遇到这种情况,只能乖乖按照后面章节的方法来解决了。理论上,最正确的做法应该是在容器加载应用程序jar包时,按照指定的顺序加载它们。通过以上分析,我们可以得出几乎所有类冲突的本质原因:要么是因为maven依赖的仲裁jar包不符合运行时要求,要么是容器类加载过程中加载的类不符合运行时要求。的。关于容器类加载隔离策略,网上有很多关于ATA的资料。本文着重讲解解决冲突的各种方法。要解决冲突,你只需要知道上面的关键原则。了解了依赖冲突的本质原因后,如何高效定位导致冲突的具体jar包呢?请继续阅读下一章。3.依赖冲突问题高效定位技巧依赖冲突主要表现为系统启动或运行过程中出现异常,99%表现为NoClassDefFoundError、ClassNotFoundException、NoSuchMethodError三种类型。下面一一解释定位技术。1NoClassDefFoundError,ClassNotFoundException排错定位步骤STEP1。当NoClassDefFoundError发生时,首先检查完整的异常堆栈,确认异常是否发生在静态代码块中。静态代码块中的异常栈和jar包冲突有明显区别。“Couldnotinitialize”,“Causedby:...”关键字一般是静态代码块中的异常导致类加载失败:因为静态代码块中出现异常导致NoClassDefFoundError,修改静态代码即可块以避免抛出异常。如果问题不是由静态代码块中的异常引起的,则继续下一步。第2步。如果不是静态代码块中的异常导致加载失败,异常消息关键字会明确显示缺失类的名称,例如:STEP3。在IDEA(快捷键Ctrl+N)中,找出异常栈中缺少哪些类版本jar包,如上例中的org.apache.commons.lang.CharUtilsSTEP4,查看应用程序上的应用程序li??b包目录部署机(通常是/home/admin/union-uc/target/${projectName}/lib或union-pub/target/${projectName}.war/WEB-INF/lib)存在找到对应版本的jar包在上一步中。出现上述情况一般是因为此时应用依赖的是低版本的jar包,并且jar包中没有冲突的类。大多数情况下,NoClassDefFoundError和ClassNotFoundException定位确认是由于maven依赖仲裁最终采用的jar包版本与运行时要求不一致导致的。2NoSuchMethodError排查步骤STEP1,发生NoSuchMethodError时,异常栈日志的核心片段(异常栈底部的片段,我见过很多同学有异常随便翻翻,没意义,没意义有目的的翻翻关键的地方,不要乱翻)会清楚的显示漏了哪个类,哪个方法。异常堆栈核心片段示例如下:在org.springframework.context.annotation.AnnotationConfigUtils.registerAnnotationConfigProcessors(AnnotationConfigUtils.java:190)在org.springframework.context.annotation.ComponentScanBeanDefinitionParser.registerComponents(ComponentScanBeanDefinitionParser.java:150)在org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parser(ComponentScanBeanDefinitionParser.java:86)atorg.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)首先需要确认JVM中当前加载的缺失方法类,如上“org.springframework.beans.factory.support.DefaultListableBeanFactory”类来自哪个jar包?目前最高效的方式:在外部环境容器中,或者某些容器版本过低不支持Arthas在线诊断时,可以在JVM启动参数中加上“-XX:+TraceClassLoading”,然后重启系统,就可以在系统工程日志中看到JVM加载类的信息,可以查到JVM是哪个jar包从加载。第2步。在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”方法。第3步。查看应用部署机器上的应用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%的依赖冲突都可以定位到rootcause。定位到原因后如何解决冲突?事实上,有时候解决冲突远没有内网很多帖子描述的“mvndependency:tree”和整理jar简单。详情请继续阅读下一章。四、通过maven调整依赖jar解决依赖冲突1、通过降级jar包解决依赖冲突接下来,只需升级pom.jar中的jar包版本即可。但是如果冲突jar包的高版本与低版本不兼容,应用依赖不是很复杂,可以分析升级冲突jar包后会影响到哪些业务。具体方法推荐使用IDEAMavenHelper插件查找存在哪些冲突的jar包。业务依赖(这里不推荐使用“mvndependency:tree”,我目前看到的大多数Maven项目都有多个Module,比如-dal,-Service,*-Controller。如果没有针对该类型单独打包上传模块项目结构Maven仓库,“mvndependency:tree”无法完整解析依赖),记录一下。如下图:然后升级冲突包,在受影响的二方库对应的业务点上通过回归测试。如果应用依赖非常复杂(比如冲突包有几十个二方库依赖,或者依赖冲突包的二方库是基础包,业务系统无法明确枚举业务点使用受影响的二方库),这种情况下,如果想通过升级jar包解决依赖冲突,则必须完整返回整个应用功能。笔者有过几次因回归不完全而导致故障的惨痛经历,希望大家不要重蹈覆辙。通过这几个案例,笔者深刻理解了当代最伟大的计算机科学家Dijkstra的名言“简单是可靠性的先决条件”,也深刻体会到如果一个系统复杂到你无法理解其错综复杂的程度根本。当存在依赖关系时,意味着你应该重构你的系统,否则系统维护将逐渐成为一场噩梦。当然,并不是所有的情况都可以通过升级升级jar来解决冲突。例如上图,假设应用系统同时依赖A.jar和B.jar,A.jar和B.jar都依赖protobuf-java。A.jar和B.jar中protobuf部分的功能是分别使用的,A.jar和B.jar所依赖的protobuf版本不能通过增减版本来调整一致。由于protobuf-java3.0版本的序列化协议,类内容与protobuf-java2.0版本在各方面不兼容。这种情况下,无论你怎么调整依赖关系,都无法解决冲突的问题。解决这个冲突,请继续阅读第五章和第六章的内容。2排除jar包解决依赖冲突。上一章的第二个例子主要是因为容器启动时加载的类不是预期的spring-beans-4.3.5.RELEASE.jar中的类,而是spring-2.5。6、对于SEC03.jar包中的类,如果排除spring-2.5.6.SEC03.jar对业务没有影响,可以通过排除spring-2.5.6.SEC03.jar解决冲突。与上一节的例子类似,可以通过IDEAMavenHelper插件判断间接依赖spring-2.5.6.SEC03.jar的jar,判断业务影响范围,这里不再赘述.与上一节一样,并非所有类似情况都可以通过排除jars来解决。5通过pandora自定义插件解决依赖冲突在第4章中提到,如果一个应用需要同时运行两个不兼容版本的jar包,那么通过Maven调整依赖是无法解决的。在第2章讲解依赖冲突原理时提到,Pandora通过类隔离机制实现了组内各个中间件之间的隔离。Pandroa也支持业务方按照规范创建可以运行在Pandora容器中的插件。容器帮助业务方实现加载隔离。联盟易淘团队根据自身业务需求,对IC、卡券等核武器级别的二方包进行裁剪打包,然后做成Pandora插件,避免依赖冲突,取得了不错的效果。使用Pandora插件确实可以完美解决依赖冲突问题,无需对应用做大的调整,也不会影响性能。但也有一些问题不适合本地方式解决,例如:当维护应用依赖过于复杂,每个应用依赖30到40个外部二方库时。这种重量级应用会严重影响生产效率。如上图所示,早期我在负责联盟用户平台的时候遇到了两个巨头应用,adv(6w+代码)和pub(12w+代码)。一方面,由于依赖较多,我们每周都会遇到群的各种升级,安全问题,各种小修,他们会不断的上线。一方面,企业出版需求多。因此,需要频繁发布。比如我一年发布了566次。这时候巨大的依赖会导致部署效率下降,影响评估和回归困难。这时候,我们不应该站在解决局部冲突的角度来看。我们应该考虑优化应用架构,进行依赖治理,尽可能避免冲突。6通过依赖架构治理解决依赖冲突1.将复杂的依赖标准化,简化治理首先,依赖本身是一个复杂的业务。大多数依赖项背后都有深厚的业务领域知识或技术领域知识。比如我们查询搜索。业务领域知识方面,仅销售额包括交易笔数、交易笔数、搜索销售额[部分订单不计入搜索销售额]等。技术领域知识方面,主要搜索和附属广告搜索引擎有时一起使用。例如,商家在进入广告前需要勾选主搜索向商家展示商品信息,进入广告后投放下链时使用广告引擎。不同引擎的调用方式有不同的结果。如下图所示,如果我们每个业务应用都单独实现,每个应用开发同学都需要消化大量与搜索客户端相关的业务和技术领域知识。成本非常高。面对这种情况,如果我们将这种复杂的依赖关系标准化包装,由专门的负责人【专人干活】,组织协作的效率会大大提高。如下所示。我们使用主要搜索和联盟引擎的统一包装。对于搜索条件,返回标准化的结果包。大大降低了学生的接入成本。以前熟悉一个引擎的访问大概需要2天。标准化的包装纸,在专人指导和规范文件指导下仅需0.5天,大大提高了效率。2重量级依赖代理服务如第5节所述,应用程序依赖的jar包过多会导致应用程序启动非常慢。所以,如果一个依赖引入了30多个jar包,就一定要提高警惕了。这种依赖导入少了,就会逐渐导致你的工作效率大幅下降。比如IC、TP、折扣中心的二方包就是典型的例子。目前我们直接为这种依赖封装了一个标准的代理服务,避免应用被这种庞大的二方包拖慢。通过以上综合治理措施,取得了良好的效果。目前联盟很少需要大家来解决矛盾。原文链接本文为阿里云原创内容,未经许可不得转载。