Java类全路径冲突解决方法

1. 问题

今天在开发中遇到这样一个问题,A同事在导入了我们的实验SDK后,发现实验无法正常获取,查看日志发现了NoClassDefFoundError异常,无法加载的的类中逻辑比较简单,只依赖了另外一个SDK包

2. NoClassDefFoundError分析和解决

一般情况下,碰到NoClassDefFoundError错误,首先我们会想到的是Maven包版本冲突了

Maven当存在多个版本的依赖时,会依赖一定的原则选取一个版本,这个版本很可能和开发环境中的版本不一致,导致一些类或者字段取不到,就会出现上面的错误

具体依赖的原则如下:

  1. 最短路径,其中A-B-C-X(1.0) , A-D-X(2.0)。由于X(2.0)路径最短,所以项目使用的是X(2.0)
  2. 顺序优先,如果A-B-X(1.0) ,A-C-X(2.0) 这样的路径长度一样怎么办呢?这样的情况下,maven会根据pom文件声明的顺序加载,如果先声明了B,后声明了C,那就最后的依赖就会是X(1.0)
  3. 覆盖优先,子pom内声明的优先于父pom中的依赖

如果出现了冲突,应该如何解决,基本是通过两种方式

  1. 排除掉不想要的版本,下面是将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>
  1. 统一版本,下面规定了此项目需要的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

发现了这个类存在于两个包中
image-20240408112400848

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

从上面的示例中可以看到

  1. 同样的代码,因为maven的顺序不同,AllowAllHostnameVerifier使用的版本也不一样,看起来是maven的优先级还是在生效
  2. 同时可以看到,两个包是可以共存的,对于不在交集中的类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,它们两个是兼容的,差别很小)

image-20240407194835146

从最短路径的原则来看呢,好像应该使用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其实是不知道的,现在我们得到下面的信息:

  1. 和maven也不是完全没有关系,因为调整顺序确实影响了使用的类
  2. 不是完全和maven jar包版本优先级规则决定

3.4 总结

在做了上面的一系列的实验之后,我还是发现了一些规律,对于相同的类名,具体使用哪个,是由jar包的顺序决定的,这里分两种情况:

  1. 如果是通过IDEA启动一个maven的java类,IDEA会根据maven的顺序来传classpath参数,使用的类必定是一个出现的jar包

image-20240407200039505

  1. 如果是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包

经过一些实验之后,可以得到的结论是:

  1. 最好的处理方法,是把冲突的包排除掉,因为大部分情况是因为代码复制改名出现的

  2. 其次,如果必须共存的话,只能依赖一个原则判断使用的类是哪个jar包中的,classpath参数的jar包的顺序、springboot生成的jar中的BOOT-INF/lib/xxx.jar顺序

  3. 如果上述的顺序不满足需要,可以调整maven中的依赖顺序来解决,可以参考这个原则

    1. 依赖在pom前面越优先
    2. 和最短路径无关
    3. 后面出现的依赖覆盖前面的依赖从而改变顺序

至于为什么jar包在前面,会优先使用其中的类,可以研究一下类加载器URLClassLoaderLaunchedURLClassLoader, 它们寻找类是从一个URL List里面遍历的,在前面的会先寻找到

参考

【1】MAVEN依赖的优先原则 - 知乎 (zhihu.com)

【2】聊一聊Springboot的类加载机制 - 简书 (jianshu.com)

posted @ 2024-04-08 11:27  songtianer  阅读(279)  评论(0编辑  收藏  举报