问题描述
应用稍复杂一点之后,往往要引入多种不同的中间件,各种第三方jar,这就导致我们往往会遇到jar包冲突的问题,如果冲突的jar包是兼容的,我们需要选择合适的版本,把不合适的版本排除掉,虽然过程复杂了点,但是如果冲突的jar包之间不兼容,那么不管选择哪个版本,都会出问题。
我们可以使用类似于OSGI这样的重框架来解决这类问题,但是这类框架太重太复杂,难以掌握,实际上我们可以利用类加载器来解决这类问题,当不兼容的情况不是很复杂时,这种方案能高效解决问题,否则需要一种插件机制来解决问题。
基本思路
类加载器双亲委派机制
这里先简单的介绍一下类加载器的双亲委派机制。JVM在加载类时默认采用的是双亲委派机制。就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;父类加载器无法完成此加载任务时,才自己去加载。我们可以看一下ClassLoader类中相关的代码:
image解决问题思路
· 通过单独的类加载器加载依赖了不兼容的jar及中间件相关的jar,使用不同的中间件时,统一入口,用不同的类加载器加载相关的类并创建对象
· 需要打破类加载器的双亲委派机制,单独创建出的类加载器要优先自己加载,加载不到则再委派给parent加载
· 被独立的classloader所托管的jar包,不能再被原来的应用类加载器加载,否则会出现类型不匹配的异常,因为jvm判断类相同的依据是类名相同并且加载类的类加载器也相同,而被单独托管后的jar已经不是原来原始的应用类加载器了
解决方案
类的加载
从URLClassLoader派生,定义一个名为ChildFirstClassLoader的类加载器,用于加载被托管的jar,代码如下:
image有了此类加载器后,我们需要在使用此类加载器前,找到指定的jar包在系统中的路径,如何找到此路径呢,有几种方法:
1. 通过System.getProperty("java.class.path")可以拿到classpath下所有的jar路径,是一个String,不同的jar通过分号分割
2. 通过类加载器,拿到URLClassLoader中的ucp,从ucp中可以拿到当前类加载器所托管的所有jar(适用于springboot)
拿到了所有jar的路径后,即可以得到某个jar包与其在系统中的路径的对应关系,然后通过此对应关系与jar包名创建出独立的类加载器(ChildFirstClassLoader),这里以使用System.getProperty("java.class.path")为例子:
image有了此类之后,我们就解决了类的加载问题,加载类的方式如下(为了说明问题,jar名随意取,实际应用中的jar不方便写出来):
private final ClassContainer container = new ClassContainer(getClass().getClassLoader(), "rocketmq-client-1.1.jar", "protobuf.jar");
在创建相关对象中,使用container.getClass(className).newInstance()的方法创建对象即可
解决类型不匹配的问题
前文提到:被独立的classloader所托管的jar包,不能再被原来的应用类加载器加载,否则会出现类型不匹配的异常,因为jvm判断类相同的依据是类名相同并且加载类的类加载器也相同,而被单独托管后的jar已经不是原来原始的应用类加载器了
为了解决这个问题,我人先解释一个概念:
· 导出类:表示ClassContainer.getClass获取的类,且希望能让原始的应用类加载器加载到的类
而解决此问题有两种思路:
1. 的方式则是利用类加载器的双亲委派机制,在双亲委派机制中插入一环,无始的类加载器的parent变成新的类加载器,新的类加载器的parent设置成原来的类加载器的parent,伪代码如下:
ClassLoader cl = getClass().getClassLoader();
ExportClassLoader ecl = new ExportClassLoader(cl.getParent());
setParent(cl, ecl);//将cl的parent设置成ecl
原始的应用类加载器(cl)在加载类时,先委派给parent加载,当委派到我们人为插入的一环时(ecl),判断是否是导出类,如果是则通过ClassContainer.getClass获取,否则继续双亲委派
2. 替换掉当前的URLClassLoader,用新的类加载器替代原有的应用类加载器
在jdk1.6时,ClassLoader中的parent字段没有定义成final字段,因此第一种思路行得通,只需要在原来的应用类加载器和它的parent之间插入一个类加载器即可,如同思路1的伪代码
但从jdk1.7开始,ClassLoader的final字段被定义成了final,此时我们只能通过代理的方式解决问题,同样创建新的类加载器ExportDelegateClassLoader,整体结构如下图:
· AppClassLoader是原始的应用类加载器
· parent表示AppClassLoader的父加载器
· ExportDelegateClassLoader是新的类加载器,用于替换原始的应用类加载器AppClassLoader,ExportDelegateClassLoader与AppClassLoader的parent相同,且classpath也相同
· ClassContainer前文描述过,不再描述
image代码如下图,在加载类时,先判断是否是导出类,如果是则使用对应的ClassContainer获取类,否则加载类,具体逻辑见loadClass方法。同样,此代码只是为了说明问题,并不是实际应用的代码:
image最后,在系统入口处用新的ExportDelegateClassLoader加载应用的入口类,以main方法为例:
image
网友评论