美文网首页
利用类加载器解决不兼容的Jar包共存的问题

利用类加载器解决不兼容的Jar包共存的问题

作者: 无醉_1866 | 来源:发表于2019-10-17 00:27 被阅读0次

    问题描述

    应用稍复杂一点之后,往往要引入多种不同的中间件,各种第三方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

    相关文章

      网友评论

          本文标题:利用类加载器解决不兼容的Jar包共存的问题

          本文链接:https://www.haomeiwen.com/subject/lzbpmctx.html