依赖冲突是平常开发中常常碰到的过程,若是运气好,并不会有什么问题。恰恰小黑哥有点背,碰到好几回生产问题,排查一整晚,最后发现倒是依赖冲突的引发的问题。java
没碰到过这个问题同窗可能没什么感受,小黑哥举两个最近碰到例子,让你们感觉一些。mysql
例子 1:spring
咱们公司有个古老的业务基础包 A。B,C 业务依赖这个包。某个团队拷贝 A 的部分代码进行重构,类名与路径彻底同样,而后从新打包成 D 发布。sql
一次业务改动,B 业务也引入了 D 包,测试环境运行的时候,一切 OK,可是在生产运行时,却抛出 NoSuchMethodError
。数据库
问题缘由在于 B 业务依赖 A,D。而 A,D 存在两个同包同名类,运行的时候,具体加载谁,不一样环境还真不同。apache
例子 2:maven
A 业务使用 Dubbo
进行 RPC
调用, Dubbo
须要依赖 javassist
。当前依赖关系为:ide
A------->Dubbo------->javassist-3.18.1.GA
复制代码
某次改动中引入另一个第三方开源包,其依赖 javassist-3.15.0-GA
。生产发布的时候,将 javassist-3.15.0-GA
打包到应用中,因为生产环节为 JDK1.8,从而致使运行直接失败。spring-boot
除了上述问题,依赖冲突还可能致使应用抛出 ClassNotFoundException
,NoClassDefFoundError
等错误。工具
抛出错误这种状况还算好,还比较容易定位问题。怕就怕,不一样版本同一个类内部逻辑不一样,从而致使业务异常。这种问题,真的很让人抓狂,让人头秃。
仔细分析依赖冲突,主要能够分为两类:
下面咱们分析一下依赖冲突产生的缘由。
Maven
依赖分为两种状况,直接依赖与间接依赖,这个比较好理解,你们直接看图就好。
若是 A 应用间接依赖多个 C 应用,且版本都不同,Maven 将会经过仲裁机制选择:
第一条原则,咱们下面再说。
第二条原则,以下图:
A 间接依赖两个版本 E,这种状况下,因为 A 到 E-1.0 路径最短,因此 A 中将会使用 E-1.0。
若是路径刚好同样,那么这种状况下 Maven
只能根据 pom
中的顺序,选择最早声明的,这也是个无奈的选择。
Maven 项目能够分为三个阶段:编译阶段,测试阶段,运行阶段了。经过 scope
属性,咱们能够决定依赖应用是否参与以上阶段,也将会影响依赖传递。
Maven
提供 6 种 scope
:
compile
provided
runtime
test
system
import
compile
compile
是 Maven
默认属性,将会使依赖包参与项目的编译,测试,运行阶段。固然,项目打包以后将会包含该依赖。
provided
provided
意味着依赖仅参与项目编译,测试的阶段。如有以下依赖关系:
A----->B----->C
复制代码
C 的 scope
为provided
,C 将会参与 B 的编译,测试阶段,可是 C 不会传递给 A。若是 A 运行过程须要 C,须要本身直接引入 C 依赖。典型如 Servlet API
,由于 Tomcat
等容器内部会提供。
runtime
runtime
表明依赖再也不参与项目编译阶段,只参与测试,运行阶段。
若依赖不参与编译阶段,这种状况 IDE 中是没法导入相应的类的。若存在依赖类,编译过程当中将会报错。
典型的例子是 JDBC
驱动包,如 mysql
:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
<scope>runtime</scope>
</dependency>
复制代码
知识点:这个好处在于,只能使用
JDBC
标准接口,这样就不会与特定的数据库绑定。后续若切换数据库,只须要更换pom
,而后修改相应的参数便可。
test
test
仅参与测试阶段的工做,典型的例子为 junit
:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
复制代码
system
system
与 provided
范围一致,只不过 system
须要使用 systemPath 属性指定本地路径,而 provided
将会从 Maven
仓库拉取。
import
import
比较特殊,不会参与以上阶段运行。其只能在 dependencyManagement
下使用,且 type
须要为 pom
。典型的例子为 Spring-boot 依赖。
<dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
复制代码
知识点:经过这种方式,解决单继承问题,也能够更好将依赖分类。
另外 Maven scope
将会影响依赖传递。
若是依赖关系为: A--->B--->C,A 依赖 B,B 依赖 C。最左列表明 B 的
scope
属性,第一行表明 C 的scope
属性
如上所示,当 C 的 scope
为 provided/test, C 只在 B 中起做用,不会经过间接依赖传递给 A。
当且仅当 B 的 scope
为 compile
,且 C scope
为 runtime
,A 将会间接依赖 C,且 scope
为 runtime
。其余状况下,C 的 scope 将会与 B 的 scope 一致。
依赖冲突时,根据错误日志,定位到冲突类,定位相应 jar
包,最后经过 excludes 排除相应的包。
另外能够结合 IDEA Maven Helper 插件,主动检查冲突依赖,提早排除。
经过插件,咱们能够清晰看到冲突包,以及依赖路径,还有相应的 Scope
。
除了排除依赖,咱们能够经过合理的设置 scope
属性,不让依赖传播下去。好比说,A 须要是使用 Spring-beans
包中某些类。若是其余项目铁定会使用 Spring,那么咱们能够将 A 中 Spring-beans
scope
设置为 provided
,让其余项目本身选择引入 Spring-beans
的版本。
这个适合公共基础包,其余包不要随便使用
provided
,若使用必定要写清楚,使用过程当中须要引入的依赖。
以上方法虽然治标,可是不治本。若是想依赖冲突不发生,咱们须要提早创建必定的规范,团队一块儿遵照,才能有效避免该类问题。
dependencyManagement
统一管理基础依赖,定义统一的版本,如经常使用中间包,工具包,日志包。snapshot
替换成正式版本。虽然 snapshot
修改起来很方便,可是正由于这个特性,能够被随便修改。若是某次生产打包发布不注意,就会引入。cmomon-lang3
是 common-lang
升级版, cmomon-lang3
包名为 org.apache.commons.lang3,而 common-lang
包名为 org.apache.commons.lang
若是咱们把 NPE
问题当作新手村普通怪物,那么依赖冲突问题就是人马这种精英怪。刚开始遇到,咱们会被虐的比较惨。只有咱们不断升级,学习掌握技巧,而后才能能够从容不迫解决。
ps:塞尔达中,大家第一次碰见人马,打了几回?小黑哥记得那天整整从晚上九点打到凌晨两点,就是打不过啊~
最后用一张思惟导图,总结文章内容。
这篇文章写的很好,你们能够看下。,从新看待Jar包冲突问题及解决方案
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客:studyidea.cn