1. 问题
今天在开发中遇到这样一个问题,A同事在导入了我们的实验SDK后,发现实验无法正常获取,查看日志发现了NoClassDefFoundError
异常,无法加载的的类中逻辑比较简单,只依赖了另外一个SDK包
2. NoClassDefFoundError分析和解决
一般情况下,碰到NoClassDefFoundError
错误,首先我们会想到的是Maven包版本冲突了
Maven当存在多个版本的依赖时,会依赖一定的原则选取一个版本,这个版本很可能和开发环境中的版本不一致,导致一些类或者字段取不到,就会出现上面的错误
具体依赖的原则如下:
- 最短路径,其中A-B-C-X(1.0) , A-D-X(2.0)。由于X(2.0)路径最短,所以项目使用的是X(2.0)
- 顺序优先,如果A-B-X(1.0) ,A-C-X(2.0) 这样的路径长度一样怎么办呢?这样的情况下,maven会根据pom文件声明的顺序加载,如果先声明了B,后声明了C,那就最后的依赖就会是X(1.0)
- 覆盖优先,子pom内声明的优先于父pom中的依赖
如果出现了冲突,应该如何解决,基本是通过两种方式
- 排除掉不想要的版本,下面是将a:b.jar包中的xx:yy.jar排除
<dependency>
<groupId>a</groupId>
<artifactId>b</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<artifactId>xx</artifactId>
<groupId>yy</groupId>
</exclusion>
</exclusions>
</dependency>
- 统一版本,下面规定了此项目需要的xx:yy.jar包版本是2.0.0,所以别的jar包中的版本不会在参考了
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xx</groupId>
<artifactId>yy</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>
于是我们通过dependencyManagament来统一了一下相关依赖,但是问题依旧没有解决
又通过观察日志,发现了是某个类缺失了一个字段INSTANCE,最终定位到了一个类AllowAllHostnameVerifier
发现了这个类存在于两个包中
3. 相同类分析和解决
这两个类的包名和类名是一模一样的,但jar包是不一样的,所以肯定不能通过上面提到的两种方式解决,它们会并存于依赖中
题外话,之所以会存在这样的jar包,是因为公司内部其他组的同事将中央仓库的包clone下来,重新命名上传到公司的仓库,这种通过复制代码然后改包名的方式提交jar包曾经见过两次,每次都是极难排查,非常不建议这样做!
如果真的碰到了这种情况,最好的方式是把其中一个给排除掉
但两个包都需要保留,因为可能每个包都有一些交集之外的类用到了,该如何解决呢?
3.1 通过Maven的顺序解决
<dependencies>
<dependency>
<groupId>httpclient</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
</dependencies>
public class ClassConflictTest {
public static void main(String[] args) {
ClassLoader classLoader = ClassConflictTest.class.getClassLoader();
URL resource = classLoader.getResource("org/apache/http/conn/ssl/AllowAllHostnameVerifier.class");
System.out.println(resource);
resource = classLoader.getResource("org/apache/http/impl/cookie/RFC6265StrictSpec.class");
System.out.println(resource);
}
}
//jar:file:/Users/a58/.m2/repository/httpclient/httpclient/4.3.2/httpclient-4.3.2.jar!/org/apache/http/conn/ssl/AllowAllHostnameVerifier.class
//jar:file:/Users/a58/.m2/repository/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar!/org/apache/http/impl/cookie/RFC6265StrictSpec.class
<dependencies>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
<dependency>
<groupId>httpclient</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.2</version>
</dependency>
</dependencies>
//jar:file:/Users/a58/.m2/repository/httpclient/httpclient/4.3.2/httpclient-4.3.2.jar!/org/apache/http/conn/ssl/AllowAllHostnameVerifier.class
//jar:file:/Users/a58/.m2/repository/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar!/org/apache/http/impl/cookie/RFC6265StrictSpec.class
从上面的示例中可以看到
- 同样的代码,因为maven的顺序不同,
AllowAllHostnameVerifier
使用的版本也不一样,看起来是maven的优先级还是在生效 - 同时可以看到,两个包是可以共存的,对于不在交集中的类
RFC6265StrictSpec
,还是会找到
3.2 最短路径不生效
如果pom中这样写
<dependency>
<groupId>xxxxx</groupId>
<artifactId>exp-client</artifactId>
<version>1.4.4</version>
<exclusions>
<exclusion>
<artifactId>spring-expression</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>httpclient</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.2</version>
</dependency>
依赖如下:(注意 : exp包用的httpclient是4.5.1,而我测试用的包是4.5.13,它们两个是兼容的,差别很小)
从最短路径的原则来看呢,好像应该使用4.3.2的类,但输出使用的是4.5.1的类,如下:
jar:file:/Users/a58/.m2/repository/org/apache/httpcomponents/httpclient/4.5.1/httpclient-4.5.1.jar!/org/apache/http/conn/ssl/AllowAllHostnameVerifier.class
jar:file:/Users/a58/.m2/repository/org/apache/httpcomponents/httpclient/4.5.1/httpclient-4.5.1.jar!/org/apache/http/impl/cookie/RFC6265StrictSpec.class
3.3 分析
其实根据maven规则来判断使用哪个类,本身就有些奇怪,因为maven主要是编译阶段的任务,把我们的依赖jar打包好,代码编译好,那运行时期选择用哪个类,maven其实是不知道的,现在我们得到下面的信息:
- 和maven也不是完全没有关系,因为调整顺序确实影响了使用的类
- 不是完全和maven jar包版本优先级规则决定
3.4 总结
在做了上面的一系列的实验之后,我还是发现了一些规律,对于相同的类名,具体使用哪个,是由jar包的顺序决定的,这里分两种情况:
- 如果是通过IDEA启动一个maven的java类,IDEA会根据maven的顺序来传classpath参数,使用的类必定是一个出现的jar包
-
如果是springboot项目,maven plugin插件也会根据maven的顺序决定jar包出现的顺序,使用的类也必定是排在前面的jar包
// 情况1 94 BOOT-INF/lib/ 95 BOOT-INF/lib/httpclient-4.3.2.jar 96 BOOT-INF/lib/httpclient-4.5.13.jar 97 BOOT-INF/lib/httpcore-4.4.13.jar 98 BOOT-INF/lib/commons-logging-1.2.jar 99 BOOT-INF/lib/commons-codec-1.11.jar 100 BOOT-INF/lib/spring-boot-2.7.1.jar 101 BOOT-INF/lib/spring-context-5.3.21.jar 102 BOOT-INF/lib/spring-aop-5.3.21.jar //情况2 94 BOOT-INF/lib/ 95 BOOT-INF/lib/httpclient-4.5.13.jar 96 BOOT-INF/lib/httpcore-4.4.13.jar 97 BOOT-INF/lib/commons-logging-1.2.jar 98 BOOT-INF/lib/commons-codec-1.11.jar 99 BOOT-INF/lib/httpclient-4.3.2.jar 100 BOOT-INF/lib/spring-boot-2.7.1.jar 101 BOOT-INF/lib/spring-context-5.3.21.jar
这个顺序一般是pom文件中jar依赖的顺序,因为解析某个jar的时候,同时会把它依赖的jar也解析,所以非最短路径也比较最短路优先,正如最短路径不优先例子中springboot jar包中的顺序如下
137 BOOT-INF/lib/swagger-annotations-1.5.20.jar
138 BOOT-INF/lib/swagger-models-1.5.20.jar
139 BOOT-INF/lib/mapstruct-1.3.1.Final.jar
140 BOOT-INF/lib/com.bj58.spat.wos.client-1.0.17.jar
141 BOOT-INF/lib/httpclient-4.5.1.jar
142 BOOT-INF/lib/commons-logging-1.2.jar
143 BOOT-INF/lib/httpcore-4.4.3.jar
144 BOOT-INF/lib/httpmime-4.5.1.jar
145 BOOT-INF/lib/json-20140107.jar
146 BOOT-INF/lib/commons-codec-1.9.jar
147 BOOT-INF/lib/junit-4.12.jar
148 BOOT-INF/lib/hamcrest-core-1.3.jar
149 BOOT-INF/lib/guava-31.0.1-jre.jar
150 BOOT-INF/lib/failureaccess-1.0.1.jar
151 BOOT-INF/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
152 BOOT-INF/lib/jsr305-3.0.2.jar
153 BOOT-INF/lib/checker-qual-3.12.0.jar
154 BOOT-INF/lib/error_prone_annotations-2.7.1.jar
155 BOOT-INF/lib/j2objc-annotations-1.3.jar
156 BOOT-INF/lib/com.bj58.zhaopin.zhuzhan.litecore-1.0.18.jar
157 BOOT-INF/lib/slf4j-api-1.7.25.jar
158 BOOT-INF/lib/httpclient-4.3.2.jar
但我稍微改一下pom,就会发现原先在前面出现的jar包又跑到后面去了,所以存在一些覆盖的问题
<dependency>
<groupId>xxxxx</groupId>
<artifactId>exp-client</artifactId>
<version>1.4.4</version>
<exclusions>
<exclusion>
<artifactId>spring-expression</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>httpclient</groupId>
<artifactId>httpclient</artifactId>
<version>4.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.1</version>
</dependency>
139 BOOT-INF/lib/mapstruct-1.3.1.Final.jar
140 BOOT-INF/lib/com.bj58.spat.wos.client-1.0.17.jar
141 BOOT-INF/lib/httpmime-4.5.1.jar
142 BOOT-INF/lib/json-20140107.jar
143 BOOT-INF/lib/junit-4.12.jar
144 BOOT-INF/lib/hamcrest-core-1.3.jar
145 BOOT-INF/lib/guava-31.0.1-jre.jar
146 BOOT-INF/lib/failureaccess-1.0.1.jar
147 BOOT-INF/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
148 BOOT-INF/lib/jsr305-3.0.2.jar
149 BOOT-INF/lib/checker-qual-3.12.0.jar
150 BOOT-INF/lib/error_prone_annotations-2.7.1.jar
151 BOOT-INF/lib/j2objc-annotations-1.3.jar
152 BOOT-INF/lib/com.bj58.zhaopin.zhuzhan.litecore-1.0.18.jar
153 BOOT-INF/lib/slf4j-api-1.7.25.jar
154 BOOT-INF/lib/httpclient-4.3.2.jar
155 BOOT-INF/lib/httpclient-4.5.1.jar
156 BOOT-INF/lib/httpcore-4.4.3.jar
4. 总结
对于这种存在相同类路径的不同jar包
经过一些实验之后,可以得到的结论是:
-
最好的处理方法,是把冲突的包排除掉,因为大部分情况是因为代码复制改名出现的
-
其次,如果必须共存的话,只能依赖一个原则判断使用的类是哪个jar包中的,classpath参数的jar包的顺序、springboot生成的jar中的BOOT-INF/lib/xxx.jar顺序
-
如果上述的顺序不满足需要,可以调整maven中的依赖顺序来解决,可以参考这个原则
- 依赖在pom前面越优先
- 和最短路径无关
- 后面出现的依赖覆盖前面的依赖从而改变顺序
至于为什么jar包在前面,会优先使用其中的类,可以研究一下类加载器URLClassLoader
和LaunchedURLClassLoader
, 它们寻找类是从一个URL List里面遍历的,在前面的会先寻找到
参考
【1】MAVEN依赖的优先原则 - 知乎 (zhihu.com)
【2】聊一聊Springboot的类加载机制 - 简书 (jianshu.com)
标签:4.5,Java,lib,BOOT,路径,jar,INF,类全,httpclient From: https://www.cnblogs.com/songjiyang/p/18120760