前言:
之前 《解决jar包冲突新思路 - maven-shade-plugin》 中,使用 maven-shade-plugin 解决了 guava 19.0 和 26.0-jre 的一个小冲突。当时在网上查到过关于使用 classloader 解决类似冲突的文章,大部分文章给出了理论解释,但是没有给出实际的代码例子,所以当时并没有完全 get 到。
最近有一些新的思考,于是还是用之前的例子,使用 classloader 解决一下,果然比 shade-plugin 的方式要好许多。
正文:
首先还是上代码: (下载链接:package-test,还是之前的项目,打了一个新分支),这次不需要多余的 package-test-b-shade 模块了。
再回顾一下问题:
与上图相对应,项目总共分3个模块:a b c,其中 a 和 b 分别依赖了guava 的 19.0 和26.0.jre ,然后c同时引用了 a 和 b 。
其中guava的两个版本有下边两个不兼容的方法,用来测试:
public static Objects.ToStringHelper toStringHelper(Object self) {
//该方法 19.0有,26.0.jre没有
}
public static String lenientFormat(
@Nullable String template, @Nullable Object @Nullable... args) {
//该方法 19.0没有,26.0.jre有
}
直接执行 com.zhaohui.C 的 test1() 方法,会报如下错误:
Exception in thread "main" java.lang.NoSuchMethodError: com.google.common.base.Strings.lenientFormat(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
at com.zhaohui.B.test(B.java:9)
at com.zhaohui.C.test1(C.java:28)
at com.zhaohui.C.main(C.java:36)
这个分支在 package-test-a 和 package-test-b 中,分别增加了 AClassLoader 和 BClassLoader 类,并且在 C 的代码中,使用这两个类加载器加载 com.zhaohui.A 和 com.zhaohui.B 这两个类:
URLClassLoader loader = AClassLoader.getInstance();
Class<?> aClass = loader.loadClass("com.zhaohui.A");
Method method = aClass.getMethod("test");
method.invoke(null);
loader = BClassLoader.getInstance();
Class<?> bClass = loader.loadClass("com.zhaohui.B");
method = bClass.getMethod("test");
method.invoke(null);
然后在 AClassLoader 设置guava 和 com.zhaohui.A 的加载路径:
//这里要换成你自己仓库的地址
public static final String GUAVA_19_0_PATH = "/Users/zhaohui/.m2/repository/com/google/guava/guava/19.0/guava-19.0.jar";
//这里要先把 package-test-a install 拿到地址以后,修改这个常量然后重新 install 一下
public static final String CLASS_A_PATH = "/Users/zhaohui/.m2/repository/com/zhaohui/package-test-a/1.0-SNAPSHOT/package-test-a-1.0-SNAPSHOT.jar";
private volatile static AClassLoader instance = null;
public static AClassLoader getInstance(){
if(instance == null){
synchronized(AClassLoader.class){
if(instance == null){
try{
URL [] urls = new URL[]{
new File(GUAVA_19_0_PATH).toPath().toUri().toURL()
,new File(CLASS_A_PATH).toPath().toUri().toURL()
};
instance = new AClassLoader(urls,Thread.currentThread().getContextClassLoader());
}catch (Exception e){
e.printStackTrace();
}
}
}
}
return instance;
}
并且设置,只要是使用 AClassLoader 这个类加载器加载类的时候,先调用 findClass() 方法在当前类加载器中加载,加载不到的才委托给双亲。
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
//优先从当前 URLClassLoader 设置的 url 中找,找不到再调用super
Class<?> cls = null;
try{
cls = instance.findClass(name);
}catch (Exception e){
}
if(null != cls){
return cls;
}
return super.loadClass(name,resolve);
}
BClassLoader 也做相同的设置以后,执行 com.zhaohui.C 的 test2() 方法,就不会报错了:
如果把 AClassLoader 和 BClassloader 中写死的常量改为通过某些约定的规则或者配置获取,然后把使用反射获取对象的代码改为SpringIoC 之类的依赖注入,这个方案应该就能轻松解决大部分 jar 包冲突了吧。
以上就是使用 classloader 解决jar包冲突的方法,真正给我灵感的是类似下边的一张图:
总结:
我一直觉得,jar包冲突,是工作中最枯燥,也是解决起来最耽误时间,最打击人的问题。每一次有了解决这个问题的新的收获,都感觉节约了未来无数的时间~~
最后,让我们保持独立思考,不卑不亢。长成自己想要的样子! (引用自 我非常喜欢的B站up主 ”独立菌儿“->猛戳链接<-的口头禅)