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

MavenJar包冲突?看看高手们是怎么解决的

时间:2023-04-01 16:06:50 Java

我接手了一个有年代感的系统。打算把重构和遇到的问题写成系列文章,让老树长出新枝,回顾一些实战技巧,分享给大家。【重构02篇】:Maven项目Jar包管理机制,冲突解决。知识背景Jar包冲突在软件开发过程中是不可避免的。因此,如何快速定位冲突的根源,了解冲突产生的过程和底层原理,是每个程序员的必修课。也是提高工作效率、应对面试、在团队中脱颖而出的机会。实践中能直观感受到的jar包冲突往往有以下几种:程序抛出java.lang.ClassNotFoundException;程序抛出java.lang.NoSuchMethodError;程序抛出java.lang.NoClassDefFoundError;程序抛出java.lang.LinkageError异常等;这可以直观地呈现出来,当然也有隐藏的异常,比如程序执行结果与预期不符。接下来我们分析一下Maven项目中Jar包的处理机制以及冲突产生的原因。MavenJar包管理机制在Maven项目中,要想了解Jar冲突,首先要了解Maven是如何管理Jar包的。这里涉及到Maven的一些特性,比如依赖转移,最短路径优先原则,先声明原则等。jar包。这样,当你在pom.xml文件中添加A的依赖时,Maven会自动为你添加所有相关的依赖。比如在SpringBoot项中,引入spring-boot-starter-web时:org.springframework.bootspring-boot-starter-web此时Maven的依赖结构可能是这样的:上面的关系,我们可以理解为依赖的传递性。即当一个依赖需要另一个依赖支持时,Maven会依次帮我们在项目中添加相应的依赖。这样做的好处是使用起来非常方便,不用一个一个去寻找依赖的Jar包。缺点是会造成Jar包冲突,后面会讲到。最短路径优先原则依赖链接1:主要根据依赖路径的长度决定引入哪个依赖(两个冲突的依赖)。例如:DependencyLink1:A->X->Y->Z(21.0)DependencyLink2:B->Q->Z(20.0)项目中同时引入了A和B的依赖,它们间接的都引入了Zdependencies,但是因为B的依赖链接比较短,Z(20.0)版本最后才会生效。这就是最短路径优先原则。这时候如果Z的21.0版本和20.0版本差别比较大,那么就会出现Jar包冲突。先声明优先原则如果两条依赖路径相同,则无法判断最短路径优先原则。这时候就需要使用先声明优先的原则,即谁先声明谁优先。例如:依赖关系1:A->X->Z(21.0)依赖关系2:B->Q->Z(20.0)A和B最终都依赖于Z。此时A的语句(按pom顺序引入)优先于B,那么冲突的Z会先引入Z(21.0),如果Z(21.0)向后兼容Z(20.0),就会有没有Jar包冲突。但是如果B语句放在前面,可能会出现Jar包冲突。Jar包冲突的原因上面提到了Maven维护Jar包的三个原则。其实每个原理都会出现什么样的Jar包冲突已经大致了解了。这是另一个综合示例。例如:依赖链接1:A->B->C->G21(guava21.0)依赖链接2:D->F->G20(guava20.0)假设项目中也引入了A和D的依赖,根据到依赖转移机制和默认的依赖调整机制(第一:优先选择最近的路径;第二:优先选择第一条语句),默认会导入G20版本的Jar包,不会导入G21的Jar包参考。如果C中的某个方法使用了G21版本中的新方法(或类),由于Maven的处理,G21还没有引入。这时候程序在调用对应的类时会抛出ClassNotFoundException异常,而在调用对应的方法时会抛出NoSuchMethodError异常。排查定位Jar包冲突问题。高版本的IDEA已经自带Maven依赖管理插件,按顺序执行:打开pom.xml文件,在文件中右击,选择Maven,选择ShowDependencies,可以查看Maven的依赖层次结构:执行后显示的效果就是原来的spring-boot-web效果。图中可以清楚的看到使用了哪些依赖,它们的级别,是否有冲突的jar包等等,冲突的部分会被标红,同时也会提示Maven默认选择了哪个版本。如果你的IDEA版本默认没有Maven管理插件,你也可以安装MavenHelper,通过这个插件可以帮助你分析Jar包冲突。安装插件重启后,打开pom.xml文件,在文件下方的DependencyAnalyzer视图中可以看到Jar包冲突的结果分析:至此,哪些Jar包冲突一目了然.同时可以在冲突的Jar包上右击执行“Exclude”进行排除,pom.xml中会自动添加排除jar包的属性。对于本地环境,可以使用MavenHelper等插件来解决,但是在预生产或者生产环境中就没有那么方便了。此时,您可以使用mvn命令定位未完成的细节。执行以下mvn命令:mvndependency:tree-Dverbose注意不要省略-Dverbose,否则忽略的包将不会显示。执行结果如下:通过这个表格,也可以清楚的看到哪些Jar包有冲突。如何统一Jar包依赖如上图所示,如果一个项目由N个子项目组成,项目之间可能存在依赖关系,Jar包冲突是不可避免的。这时候可以使用父pom来统一管理版本,一劳永逸。通常的做法是在父模块的pom文件中尽可能声明所有相关的依赖Jar包的版本,在子pom.xml中简单引用(不再指定版本)组件。比如在父pom.xml中定义Lombok的版本:org.projectlomboklombok1.18.10可以在子模块中定义如下:org.projectlomboklombok这样所有的子模块都采用统一的版本。Jar包冲突的解决方法下面介绍几种基于Maven项目的几种场景下Jar包冲突的解决方法:Maven默认处理:使用该方法,必须牢记Maven依赖调整机制的基本原则,最接近的路径优先,第一个声明优先;排除方法:上面MavenHelper的例子中提到,可以通过exclude在pom.xml中排除冲突的Jar包;版本锁定法:如果项目依赖同一个Jar包的多个版本,排除起来很麻烦,这时候可以使用版本锁定法,即直接显式引入指定版本的依赖。根据前面介绍的Maven处理Jar包的基本原理,这个方法的优先级最高。这种方式一般采用我们上面提到的统一Jar包依赖的方式。Jar包冲突的本质上面提到了Maven在项目中对Jar包冲突的解决原则和实际解决方案,但并未涉及到Jar包冲突的本质。Jar包冲突的本质,在《从Jar包冲突搞到类加载机制,就是这么霸气》一文中已经有详细的讲解。以下是一些关键点的概述。Jar包冲突的本质:由于某些因素,Java应用程序无法加载正确的类,导致其行为与预期不一致。具体有两种情况:情况一:项目依赖同一个Jar包的多个版本,选择了错误的版本;情况2:同一个类出现在不同的Jar包中,导致JVM加载错误的类;案例一,也是本文重点关注的场景,即引入了多个Jar包版本,不同的Jar包版本有不同的类和方法。由于(不理解)Maven的依赖树的仲裁机制,导致Maven加载错误的Jar包,导致Jar包冲突;第二种情况,同一个类出现在不同的Jar包中(上一篇有详细介绍)。这种情况是因为JVM的同一个类加载器只会加载同一个类一次,而现在加载完一个类之后,就不会再加载同一个全限定名的类,从而导致Jar包冲突的问题。对于第二种情况,如果不是类冲突抛出异常,你可能连意识都没有意识到,所以就更加棘手了。这种情况可以通过分析不同类加载器的优先级和加载路径,文件系统的文件加载顺序等来解决,如前所述。总结除了以上方法,还有很多排查类冲突的小技巧,比如使用IDE提供的搜索功能,直接搜索抛出异常的类,看是否有多个,你是否正在使用预期的版本。这些技能需要在实践过程中不断摸索和积累。总之,不管项目有多大,依赖关系有多复杂,只要牢记冲突的起因和几种解决冲突的方法,仔细分析,总会有迹可循的。看完本文并实践,你或许会在团队中脱颖而出,成为Jar包冲突的终结者。博主简介:《SpringBoot技术内幕》技术书籍作者,热爱研究技术,撰写技术文章。公众号:《程序新视界》,博主的公众号,欢迎关注~技术交流:请联系博主微信号:zhuan2quan