当前位置: 首页 > 科技观察

程序员需要了解依赖冲突的原因和解决方法

时间:2023-03-18 16:35:37 科技观察

前言依赖冲突是日常开发中经常遇到的一个过程。如果你幸运的话,就不会有问题。不过阿芬有点懵,生产上遇到了好几个问题。经过一晚上的排查,终于发现是依赖冲突导致的问题。没有遇到过这个问题的同学可能感受不到。阿芬就举两个最近遇到的例子让大家感受一下。例1:我们公司有一个老的业务基础包A,B、C业务依赖于这个包。某团队复制A的部分代码进行重构,类名和路径完全一致,然后重新打包到D中发布。A业务变更,B业务也引入了D包,测试环境跑的时候一切正常,生产跑的时候就抛出NoSuchMethodError。问题的原因是B的业务依赖于A和D,但是A和D在同一个包中有两个同名的类。运行的时候,不同环境加载谁,真的是不一样。示例2:某业务使用Dubbo进行RPC调用,Dubbo需要依赖javassist。目前的依赖是:A-------->Dubbo-------->javassist-3.18.1.GA在某次改动中又引入了一个第三方开源包,依赖javassist-3.15。0-遗传算法。当产品发布时,javassist-3.15.0-GA被打包到应用程序中。由于生产环节是JDK1.8,直接运行失败。除了上述问题,依赖冲突还可能导致应用程序抛出ClassNotFoundException、NoClassDefFoundError等错误。抛出错误也不错,更容易定位问题。就是怕,同一个类在不同版本的内部逻辑不一样,导致业务异常。这种问题真是让人抓狂,让人秃头。仔细分析依赖冲突主要分为两类:同一个项目依赖应用,存在多个版本。每个版本都是同一个类,可能会有差异。不同的项目依赖于应用程序,存在包名和类名完全相同的类。下面分析一下依赖冲突的原因。1依赖冲突产生的原因1.1依赖机制Maven的依赖分为两种情况,直接依赖和间接依赖。这个比较容易理解,直接看图就可以了。1.2仲裁机制如果A应用间接依赖多个C应用,且版本不同,Maven会通过仲裁机制进行选择:当根据依赖管理元素中指定的版本声明优先进行仲裁时,遵循以下两个原则:invalidShortPathpriority如果路径相同,它将查看pom.xml中声明的顺序。第一个原理,我们下面讲。第二个原则,如下图所示:A间接依赖两个版本E。此时,由于A到E-1.0的路径最短,所以A中会使用E-1.0。如果路径正好同理,这种情况Maven只能按照pom中的顺序选择最先声明的,也是无奈的选择。1.3范围属性Maven项目可以分为三个阶段:编译阶段、测试阶段、运行阶段。通过scope属性,我们可以决定依赖的应用是否参与上述阶段,这也会影响依赖转移。Maven提供了6个作用域:compileprovidedruntimetestsystemimportcompilecompile是Maven的默认属性,它会让依赖包参与到项目的编译、测试和运行阶段。当然这个依赖会在项目打包后包含进来。providedprovided表示依赖只参与项目的编译和测试阶段。如果有如下依赖:A----->B----->CC提供scope,C会参与B的编译和测试阶段,但C不会传递给A。如果需要CA的运行过程,需要自己直接引入C的依赖。一个典型的例子就是ServletAPI,因为它是由Tomcat等容器在内部提供的。runtimeruntime表示依赖不再参与项目编译阶段,只参与测试和运行阶段。如果依赖不参与编译阶段,则此时IDE中无法导入对应的类。如果有依赖类,编译时会报错。典型的例子是JDBC驱动包,比如mysql:mysqlmysql-connector-java6.0.6runtime知识点:这样做的好处是只能使用JDBC标准接口,不会绑定到特定的数据库。如果后面切换数据库,只需要改一下pom,然后修改相应的参数即可。testtest只参与测试阶段,典型的例子是junit:junitjunit4.12testsystemsystem与provided范围一致,只是system需要使用systemPath属性指定本地路径,provided会从maven仓库拉取。importimport比较特殊,不会参与上述阶段。只能在dependencyManagement下使用,类型需要是pom。典型的例子就是Spring-boot依赖。org.springframework.bootspring-boot-dependencies2.1.6.RELEASEpomimport知识点:这样可以解决单继承的问题,依赖也可以更好的分类。另外,Maven作用域会影响依赖传递。如果依赖关系是:A--->B--->C,则A依赖B,B依赖C。最左边一列代表B的scope属性,第一行代表C的scope属性。如上所示,当提供/测试C的作用域时,C只作用于B,不会通过间接依赖传递给A。当且仅当B的作用域是编译,C的作用域是运行时,A会间接依赖C,作用域是运行时。在其他情况下,C的范围将与B的范围一致。2.解决冲突的方法2.1使用Maven属性控制依赖传递依赖冲突,根据错误日志定位冲突的类,定位到对应的jar包,最后通过excludes排除对应的包。另外可以结合IDEAMavenHelper插件,主动检查冲突依赖,提前排除。通过插件,我们可以清楚的看到冲突的包,依赖路径,以及对应的作用域。除了排除依赖之外,我们还可以通过合理设置scope属性来防止依赖传播。比如A需要用到Spring-beans包中的一些类。如果其他项目肯定会使用Spring,那么我们可以将A中的Spring-beansscope设置为provided,让其他项目选择引入Spring-beans的版本。这适用于公共基础包。不要随便为其他包使用provided。使用的话一定要把使用过程中需要引入的依赖写清楚。以上方法虽治标不治本,但治本不治本。如果我们要避免依赖冲突,我们需要提前建立一定的规范,团队可以共同遵守,才能有效避免此类问题。在应用项目中,使用dependencyManagement统一管理基础依赖,定义统一的版本,比如常用的中间包、工具包、日志包等。不要在二方包中引入不相关的依赖,尽量少的依赖。在团队开发中,比较常见的是二方包继承publicparentpom,导致继承了很多不相关的依赖,可以单独管理。二方包要向下兼容,不要随意改变已有的类名、方法名、字段名。项目申请上线前,将快照替换为正式版。虽然快照修改起来很方便,但是因为这个特性,可以随意修改。如果生产包装发布不注意,则会引入。请勿对二方包使用相同的包名或类名。一般来说,在团队开发中,包名和类名相同的概率比较小。这个在一些重构项目中比较容易出现,复制原来的类,重构打包发布。在某些情况下可以修改包名称。例如cmomon-lang3是common-lang的升级版,cmomon-lang3的包名为org.apache.commons.lang3,common-lang的包名为org.apache.commons.lang3。综上所述,如果我们把NPE问题当成普通怪物,那么依赖冲突问题就是半人马的精英怪物。刚见面的时候,我们会被虐的更惨。只有不断升级,不断学习,掌握技能,才能从容解决。ps:在塞尔达,第一次见人,打了多少次?阿芬记得那天晚上9:00打到凌晨2:00,你就是打不过他们~4.帮助文档MavenDependencyScopesMavenoptionalkeywordsarethoroughdiagram