jar依赖冲突解决实践
前言
随着功能的增多,各种中间件的引入。应用以来的各种jar的规模极具膨胀,出现jar冲突和Class冲突的问题层出不穷,让人不胜其扰。本文针对冲突,提供一个排查和定位问题的最佳实践。实践中尽量不借助第三方工具,而使用maven或者Linux的自带命令行。
Maven构建的应用的jar冲突
目前最为最流行的项目构建和管理工具,在目前的互联网应用中被广泛使用。maven框架很大的一个便利就是对于jar的依赖管理,它自然提供了一些工具帮助开发者进行依赖分析。maven存在坐标的概念。groupId,artifactId,version三个维度定位到一个唯一的jar。
1234567 |
|
对于版本,有一个很宽泛的范围
[3.6.0,4.0.0) 要求的依赖版本>=3.6.0且<4.0.0
[,3.6.0] 要求的依赖版本<=3.6.0
对于应用来讲,还是固定一个版本为好,夸版本有太多不可预知的情况存在。静态代码检查
通过mvn dependency:tree 命令查看依赖树
12345678910111213141516 | [INFO] \- com.alibaba.china.shared:credit_shared.ruleengine.biz:jar:1.0-SNAPSHOT:compile [INFO] +- com.alibaba.china.shared:credit_shared.ruleengine.api:jar:1.0-SNAPSHOT:compile [INFO] +- com.alibaba.china.shared:credit_shared.ruleengine.dal:jar:1.0-SNAPSHOT:compile [INFO] +- org.antlr:antlr:jar:3.3:compile [INFO] +- org.antlr:antlr-runtime:jar:3.3:compile [INFO] | \- org.antlr:stringtemplate:jar:3.2.1:compile [INFO] +- org.mvel:mvel2:jar:2.1.3.Final:compile [INFO] +- org.drools:knowledge-api:jar:5.5.0.Final:compile [INFO] +- org.drools:drools-core:jar:5.5.0.Final:compile [INFO] | \- org.drools:knowledge-internal-api:jar:5.5.0.Final:compile [INFO] \- org.drools:drools-compiler:jar:5.5.0.Final:compile [INFO] +- org.antlr:antlr:jar:2.7.7:compile [INFO] +- org.eclipse.jdt.core.compiler:ecj:jar:3.5.1:compile [INFO] \- com.thoughtworks.xstream:xstream:jar:1.4.1:compile [INFO] +- xmlpull:xmlpull:jar:1.1.3.1:compile [INFO] \- xpp3:xpp3_min:jar:1.1.4c:compile |
通过静态代码扫描的方式,能分析出来jar之间的依赖关系。举例credit_shared.ruleengine.biz-1.0-SNAPSHOT.jar依赖了antlr-3.3.jar。org.antlr:antlr-3.3.jar在maven中的坐标是
1234567 |
|
依赖仲裁
从上面的依赖树,出现了另外一个版本的jar——org.antlr:antlr:jar:2.7.7:compile,这就出现了依赖仲裁的问题。
maven 2.2.1版本仲裁规则:- 按照项目总POM的DependencyManager版本声明进行仲裁(覆盖),但无警告
- 如无仲裁声明,则按照依赖最短路径确定版本
- 若相同路径,有严格区间限定的版本优先
- 若相同路径,无版本区间,则按照先入为主原则
如要解决冲突问题,很多时候都用到exclusions,如A->B->D(v1),A->C-D(v2),要指定A->D(v1),则需要在声明C的依赖时候通过exclusions列表排除掉对D(v2)的依赖。
要更好理解依赖仲裁,需要了解以下附带知识。
maven classpath
maven中有三种classpath:
- 编译classpath:编译项目代码,依赖的jar会被引入到classpath
- 测试classpath:编译和执行测试部分代码,如单元测试,集成测试,依赖会被引入到classpath
- 运行classpath:实际运行代码的时候,依赖的jar会被引入到classpath
scope:依赖范围
scope就是为了解决jar在classpath中的可见性。scope有以下几个可选项
compile:默认值,对编译classpath、测试classpath、运行classpath都有效,在三个阶段都需要指定的jarprovided:编译和测试可用,不会被传递依赖,不会被打包。例:依赖于web容器中的提供的一个jar包,在编译的时候需要加入依赖(web容器还没有介入),运行的时候由web容器来提供。如servlet-api。test:执行单元测试时可用,不会被打包,不会被传递依赖runtime:运行和测试时需要,但编译时不需要。最典型的例子是JDBC的驱动,编译时只需要提供驱动的API即可,在运行和测试阶段,需要加载到具体的驱动实现。system:跟provided一致,显示制定依赖路径,一般是指定了本地的仓库之外的类库文件。可能造成不可依赖性,不推荐使用。传递性依赖
A->B,B->C,则A->C。这是传递性依赖。依赖是有范围的,A->B,B->C的依赖范围决定了A->C的依赖范围。
A->C | compile | provided | test | runtime | |
---|---|---|---|---|---|
compile | compile | runtime | |||
provided | provided | provided | provided | ||
test | test | test | |||
runtime | runtime | runtime |
可选依赖
12345678 |
|
A->B,B->C(可选),B->D(可选),则A不会通过传递依赖到C或者D。
非Maven项目或者不同坐标的jar出现Class冲突,
上面介绍的是jar相同而版本不同,如antlr-3.3.jar,antlr-3.2.jar类似情况的冲突解决方案。这种情况一般出现在中间件升级。下面介绍坐标不同,如antlr-old-3.3.jar、antlr-new.jar,而jar中包含了类路径完全相同的类的情况。
出现这种情况,一般的异常提示都是“XXX类has no such method XXXX”之类的。这些异常提示基本可以定位成Class不是你要的Class,只不过Class的全路径是相同的,最大的可能也就是ClassLoader加载了另外一个jar的同名类。所以,首先要排查到该类是从哪个具体jar中来的。JVM提供了这样的功能,查看加载类的情况 1 | java -verbose:class |
类加载情况,如SubscriptionInfo类加载自file:/home/zhao/web-deploy/jettyserver/tmp/jetty-0.0.0.0-34200-root.war--any-/webinf/WEB-INF/lib/pc2.common-1.2.5.jar
12 | [Loaded com.alibaba.pc2.common.remote.subscription.SubscriptionInfo from file:/home/zhao/web-deploy/jetty_server/tmp/jetty-0.0.0.0-34200-root.war-_-any-/webinf/WEB-INF/lib/pc2.common-1.2.5.jar] [Loaded com.alibaba.pc2.common.domain.productpackage.PackageInfo from file:/home/zhao/web-deploy/jetty_server/tmp/jetty-0.0.0.0-34200-root.war-_-any-/webinf/WEB-INF/lib/pc2.common-1.2.5.jar] |
而同样在pmap pid可以进程的内存镜像。 jps -v查看pid,再通过pmap pid > map.txt,从map.txt中查到
1 | 00007fa4ae174000 20K r-xs- /home/zhao/web-deploy/jetty_server/tmp/jetty-0.0.0.0-34200-root.war-_-any-/webinf/WEB-INF/lib/pc2.common-1.2.5.jar |
得到确认后,如果是maven工程,则通过依赖树查询到是具体哪个jar依赖了这个错误引用,在pom文件中exclusions掉该jar即可。
如果是非maven工程,则通过其他方式把错误引用排除掉即可。