关于java:Java依赖冲突高效解决之道

57次阅读

共计 7161 个字符,预计需要花费 18 分钟才能阅读完成。

简介:因为阿里妈妈联盟团队负责业务的特殊性,零碎有宏大的对外依赖,依赖团体六七十个团队服务及 N 多工具组件,通过此文和大家分享一下咱们积攒的一些简单依赖无效治理的教训,除了简略技术技巧的总结外,也会探讨一些对于这方面架构的思考,心愿此文能零碎彻底的解决 java 依赖抵触对大家的困扰。

作者 | 澄江
起源 | 阿里技术公众号

一 概述

因为阿里妈妈联盟团队负责业务的特殊性,零碎有宏大的对外依赖,依赖团体六七十个团队服务及 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: …” 关键字个别是动态代码块产生异样导致类加载失败:

因为动态代码块产生异样导致 NoClassDefFoundError,批改动态代码块防止抛出异样即可。如果不是动态代码块产生异样导致的问题,持续下一步。

STEP2、如果不是动态代码块产生异样导致加载失败,异样 message 关键字中会明确显示缺失的类名称,例如:

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 那么简略。具体细节请持续看下一章节。

四 通过 maven 调整依赖 jar 解决依赖抵触

1 升降级 jar 包解决依赖抵触

上一章节中的第一个例子中,最简略的状况,如果发生冲突的 jar 包高版本是齐全兼容低版本性能的状况下,只需在 pom 中简略降级 jar 包版本即可。

但如果抵触 jar 包高版本不兼容低版本,且利用依赖不是很简单的状况下,能够剖析降级抵触 jar 包后会对哪些业务有影响,具体做法举荐通过 IDEA Maven Helper 插件查找抵触 jar 包有哪些业务依赖(此处不举荐 ”mvn dependency:tree”,目前自己见过的大部分 Maven 工程都有多个 Module,比方 -dal,-Service,*-Controller,这类工程构造如果 module 未独自打包上传 Maven 仓库,”mvn dependency: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 来解决抵触。与上一节例子相似,能够通过 IDEA Maven Helper 插件确定 spring-2.5.6.SEC03.jar 是由哪个 jar 间接依赖进来的,判断业务的影响范畴,此处不在赘述。与上一节一样,相似的状况不肯定都能够用排除 jar 解决。

五 通过 pandora 自定义插件解决依赖抵触

第四章中有讲到,如果一个利用中要同时运行两个不兼容版本的 jar 包,是无奈通过 Maven 调整依赖关系解决的。第二章解说依赖抵触原理时有提到,Pandora 通过类隔离机制实现了团体各个中间件之间的隔离,Pandroa 同时也反对业务方按标准创立一个能够运行在 Pandora 容器中的插件,容器帮业务方实现加载隔离。

联盟一淘团队就将相似 IC、卡券这种核武器级存在的二方包依据本人业务的须要进行裁剪包装后,制作成 Pandora 插件来防止依赖抵触,获得了很好的成果。

用 Pandora 插件的确能在不对利用做很大调整,不影响性能的状况下完满解决依赖抵触问题。

但也有一些问题就不太适宜用部分办法解决了,比方:

当保护的利用依赖过于简单,每个利用依赖内部三四十个二方库时。这种重量级利用就会重大影响生产效率。

如上图所示,晚期自己负责联盟用户平台时,就遇到两个巨无霸利用,adv(6w+ 代码)、pub(12w+ 代码)。

一方面因为依赖多,根本每周都会遇到团体各种降级,平安问题,各种小修小补,一直的上线。一方面业务公布需要也较多。

导致须要频繁公布,比方有一年集体就公布了 566 次。此时宏大的依赖导致部署效率,影响评估回归都会很难,此时就不应该从部分解决抵触这种视角去看,应该思考优化利用架构,进行依赖治理,尽量避免抵触。

六 通过依赖架构治理解决依赖抵触

1 简单依赖标准化、简化治理

首先,依赖自身就是一种简单的业务。大部分依赖背地都有较深的业务畛域常识 或者 技术畛域常识。

比方咱们查问搜寻。

业务畛域常识方面,光销量就有交易成交笔数,成交件数,搜寻销量【有些订单不计入搜寻销量】等。

技术畛域常识方面,主搜寻,联盟广告搜索引擎有时是配合应用的,比方商家未入驻广告前给商家展现货品信息就须要查主搜寻,而入驻后投放上行时则须要用广告引擎。不同引擎的调用办法,后果都不一样。

如下图所示,如果咱们每个业务利用都各自实现,那么各利用开发同学就要消化大量搜寻客户端相干的业务、技术畛域常识。老本是很高的。

面对这种状况,如果咱们将这类简单的依赖,由专人 owner 进行对立包装标准化【专人干专事】,会大大晋升组织协同效率。如下图所示。

咱们通过对主搜寻,联盟引擎的对立封装。对检索条件,返回后果的标准化封装。大大降低了同学们的接入老本,以往要相熟一个引擎的接入大略要 2 天,用标准化封装后的 wrapper,在专人,标准文档的领导下仅 0.5 天就能够,大大晋升效率。

2 重量级依赖代理服务化

第五节中有讲到,利用依赖的 jar 包过多会导致利用启动很慢,因而如果一个依赖引入 jar 包超过 30 个以上时,务必要警觉,这种依赖引入几个,就会逐渐导致你工作效率大大降落。比方 IC,TP,优惠核心的二方包就是典型的例子。

目前咱们针对这类依赖,是间接封装一个规范代理服务,防止利用被这种巨无霸二方包拖慢。

通过以上综合治理伎俩,获得了很好的成果。目前联盟很少再须要大家去解决抵触问题。

原文链接
本文为阿里云原创内容,未经容许不得转载。

正文完
 0