在我们实际开发过程中,我们往往会遇到需要引用第三方的jar包,或者说是合作方的SDK来达到快速完成与第三方应用的对接。但是单在我们的项目中引入对方提供的jar包时,总是会遇到相关基础能力jar包的版本冲突,又或者权限定类名冲突,在对方没有自定义类加载器的时候,我们是需要解决这些冲突问题的,不然再项目运行时就会发生找不到类或者找不到具体的方法。
常见的jar包冲突的2种异常:
- java.lang.NoSuchMethodError;
- java.lang.ClassNotFoundException;
冲突的来源
对于这种问题相信大家时长遇到,当我们在使用一些开源工具时,他们内部引入的基础能力jar包版本往往会不一致,如:
api("org.bouncycastle:bcprov-jdk15on:1.70")
api("org.springframework.security:spring-security-crypto:5.3.13.RELEASE")
像上面这两个依赖在spring-security中是有引入api(“org.bouncycastle:bcprov-jdk15on:1.64”)的,而我们在自己的项目用又用到了该jar包的1.70版本,这个时候就发生了jar包冲突。
以下解决方法都是以此冲突案例为示例:
jar冲突的解决方案
1.排除不想使用的版本
一般构建工具会帮我们解决,选择一个合适的版本就行编译构建,但是为了保险起见我们一般会选择一个兼容性更高的版本,即技能满足spring-security的核心能力,有能满足我们的开发需求。所以我们可以排除spring-security中引入的改jar依赖,以我们的高版本为准。(只是举例,实际以自己的开发环境为准)
api("org.springframework.security:spring-security-crypto:5.3.13.RELEASE"){
exclude groupId: 'org.bouncycastle'
}
2.强制指定jar包版本
往往我们冲突的jar包不单单是来至于某个开源jar,可能是多个,比如我们项目需要对接某个银行,银行的接入方式,提供一个SDK来嵌入到我们的项目中,然后我们通过调用他们SDK的方法来实现远程http调用。
不巧他们的SDK中也引入了一个该加密工具包:
api("org.bouncycastle:bcprov-jdk15on:1.69")
此时我们在引入银行的SDK时,有需要排掉org.bouncycastle这个groupId,如果再来一个SDK又得排除。这样就显得比较麻烦,所以就有了强制指定jar的版本。
configurations.all {
resolutionStrategy {
//强制依赖
force 'org.bouncycastle:bcprov-jdk15on:1.70'
}
}
3.maven-shade-plugin重新构建新jar包
以上两种方法能解决问题都是建立在高版本兼容了历史版本的情况下,但是当我们遇到版本互不兼容时,又得在同一个项目中同时使用,这个时候我们排掉任何一个版本都不行,都会导致某个功能无法使用。此时就可以使用到maven-shade插件来帮我们解决。
**场景:**银行提供的SDK里面使用的是 org.bouncycastle:bcprov-jdk16:1.46,而我们项目中使用的是spring-security内依赖org.bouncycastle:bcprov-jdk15on:1.46。虽然moudle不同,但是 org.bouncycastle的类路径是一样的,由于JVM类加载机制,全限定类名与类加载公共决定一个类的Class。无论是排掉那个依赖,最终都有一处会包异常。
**解决方法:**利用maven-shade插件加银行的SDK引入在引入之前冲突的加密工具类型,然后引入打包插件,具体配置文件内容如下,最后执行MVN的打包命令,或者在IDEA上直接点package,生成sdk-api-lib-1.0.0.jar。然后再在自己的项目中引入该jar就解决了org.bouncycastle的冲突问题。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!--在高低版本间有冲突时,使用其中任何一个版本都无法解决问题时,可以采用重写jar包的方式,
修改指定包名,使其可以被jvm所加载,从而解决jar包冲突的问题。
缺点就是:1.同样能力的类,由于全路径被修改,JVM需要加载多个,占用内存
2.只能保证解决单个jar包的基础依赖冲突,如果有多个jar包使用了同样的基础依赖,如本文的
org.bouncycastle:bcprov-jdk16:1.46,那个所有依赖这个基础依赖的jar包都需要重新打包
更换基础依赖的类路径。
-->
<groupId>com.bank.sdk</groupId>
<artifactId>sdk-api-lib</artifactId>
<version>1.0.0</version>
<name>sdk-api-lib</name>
<dependencies>
<dependency>
<groupId>com.bank.sdk</groupId>
<artifactId>sdk-api</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 指定项目java编译版本 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.4.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>true</createDependencyReducedPom>
<relocations>
<relocation>
<pattern>org.bouncycastle</pattern>
<shadedPattern>shaded.org.bouncycastle</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
原理:最后说下原理,其实很简单,就是把org.bouncycastle开头的所有类全部换成了shaded.org.bouncycastle重新生成了一并打入了新的jar包(sdk-api-lib-1.0.0.jar)中。此时改SDK就不需要依赖org.bouncycastle:bcprov-jdk16:1.46,直接使用自己自己本地的shaded.org.bouncycastle。
相当于修改了冲突类的全限定名,是的JVM能同时加载之前冲突的类。可以理解成将原来的加密工具jar自己改成一个包路径重新打了一个具备相同能力的jar包。
缺点:1、显而易见,同样能力的类加载了两次,浪费内存。2、后续遇上使用org.bouncycastle:bcprov-jdk16:1.46或者其他版本,只要有冲突又得重新再来一次,不能一劳永逸。
以上三种方法基本能解决99%的jar冲突问题,实在遇上解决不了的,那就直接新建项目,或者拿到jar包源码直接进行修改,重新再打包
以上是在实际工作中遇到jar冲突时,费了九牛二虎之力才最终使用第三种方法解决,在此记录下,希望对你有帮助。