类加载器 ClassLoader 实现 jar 文件版本加载隔离验证样例

随着公司的发展,业务面临的复杂环境也多了,需求真是越来越难做了。

最近需要重构一个模块,暂且叫 ProjectA 吧,因为某些原因,ProjectA 需要依赖大量的第三方组件,而第三方组件都是又有一堆常用开源库的依赖。

Java 的生态圈确实比较丰富啊,但是此情此景,这让我想起了一张图,真是传神了!

原来的解决思路是挑出所有的公共开源库,编译进 ProjectA,然后第三方组件根据需要动态的加载和卸载,避免同包同名类的冲突。

常见的冲突就是类似于现有两个开源库,一个叫 iolib,一个叫 enclib,这两个开源作者将包名都定义为 com.github,这是常有的事情,并不奇怪。然后两个人心照不宣的都定义了一个工具类,全路径为 com.github.tool.StringUtils。

现在 iolib 和 enclib 都动态加载到内存里了,这简直就是一个灾难!因为你根本不知道现在调用的究竟是哪个鬼。

还有一个情况是,现在 iolib 的作者对库进行升级,版本到了 v2.0,并且重构了 StringUtils 类。

假设原来有一个判断字符是否为空的方法定义:

public boolean isEmpty(String data);

类库作者在重构的时候非常失误,没有考虑兼容性,这也是常有的事情,并不奇怪。

作者的原意是,想在判断的时候加入一个布尔参数:是否将空格作为空的情况返回。

public boolean isEmpty(String data, boolean blank);

现在的问题是 ProjectA 模块依赖的第三方组件 A 和 B,都依赖了类库 iolib,但是 A 依赖了 iolib 版本 V1.0,B 依赖了版本 V2.0,并且组件 A 和 B 有可能同时被加载到内存中。

这么狗血的事情是可能的!我天天就处理这么狗血的事情,并且事实上实际情况比这里的举例复杂的多。

所以最近一直在思考重构的事情,去年入手的周志明的《深入理解 Java 虚拟机》大致的看了一遍,收获颇深。这次又拿出来把有关类加载器的两章深入的研究了一下。

类加载器(ClassLoader)

把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放在 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块成为”类加载器“。

摘自周志明的《深入理解Java虚拟机》

ClassLoader 常见应用场景

  • 功能测试

每个加载器,有自己的独立的类名称空间。

比较两个类是否“相等”的前提是它们是由同一个类加载加载才有意义,即 ClassLoader 如果不同,两个类必定不等。这样使得在一个 JVM 中加载同一个模块的不同版本的 jar 成为现实,基于反射功能,我们同样可以很轻松实现不同版本的模块测试。本文后面会提供简单 demo 的实现。

  • 代码加密

对 class 文件进行混淆、压缩、native 等等手段后的解密过程。

  • OSGi

是动态模型形同,在 eclipse 中插件的实现就是基于 OSGi 思想,而 eclipse 主要的应用就是插件,所以可以理解为 eclipse 插件是 OSGi 的应用典范。

  • 热部署

不停止服务,动态替换目标文件。ClassLoader 动态加载 jar 包,如果做一个工程化的东西可能会费些周章,但是原理并不复杂。周志明的《深入理解 Java 虚拟机》中有相关演示。

  • 容器

像 Tomcat 这些 Java 是实现的 J2EE 的容器,需要管理容器本身依赖的类库和部署的项目类库,实现部分类库共享,以及不同项目之前类库的访问隔离,都是通过类加载器来实现的。感兴趣的话可以参阅 Tomcat 的源代码。

ClassLoader 很重要,Java 世界需要它!

预研样例

为了验证方案的可行性,我进行了一个简单的预研。

本地生成了 JavaLibrary1.jar 和 JavaLibrary2.jar 两个 jar 包。这两个 jar 都有类 com.daimafans.lib.HelloWorld,此 Demo 要实现的是同时将这两个 jar 包的同名类加载到 JVM 并且各自执行。

首先创建了三个工程,JavaLibrary1 工程生成 JavaLibrary1.jar 库,JavaLibrary2 工程生成 JavaLibrary2.jar 库。

ClassLoaderTest 工程做一个单元测试。

JavaLibrary1

HelloWorld.java 类,很简单,对输入的人名说 Hello。

package com.daimafans.lib;

/**
 * Libary Hello World
 *
 * @author liuqianfei
 */
public class HelloWorld
{
    public void sayHello(String name)
    {
        System.out.println("Hello," + name + "!");
    }
}

JavaLibrary2

有两个输入参数,除了对输入的人名说 Hello,还会打一次招呼。

package com.daimafans.lib;

/**
 * Libary Hello World
 *
 * @author liuqianfei
 */
public class HelloWorld
{
    public void sayHello(String name, String greeting)
    {
        System.out.println("Hello," + name + "!" + greeting);
    }
}

Test

测试方法将两个 jar 包通过 ClassLoader 加载到内存后,通过反射分别调用 sayHello 方法。

@Test
public void testLoadJar()
        throws MalformedURLException, ClassNotFoundException, NoSuchMethodException,
        IllegalAccessException, IllegalArgumentException, InvocationTargetException, InstantiationException
{
    ClassLoader loader1 = new URLClassLoader(new URL[]
    {
        new URL("jar:file:E:\\nb-workspac\\ClassLoaderTest\\repo\\JavaLibrary1.jar!/")
    }, getClass().getClassLoader());

    ClassLoader loader2 = new URLClassLoader(new URL[]
    {
        new URL("jar:file:E:\\nb-workspace\\ClassLoaderTest\\repo\\JavaLibrary2.jar!/")
    }, getClass().getClassLoader());

    String className = "com.daimafans.lib.HelloWorld";

    // loader1
    System.out.print("JavaLibrary1.jar \t");
    Class clazz1 = Class.forName(className, true, loader1);
    clazz1.getMethod("sayHello", String.class).invoke(clazz1.newInstance(), "Sid Lau");

    System.out.println();

    // loader2
    System.out.print("JavaLibrary2.jar \t");
    Class clazz2 = Class.forName(className, true, loader2);
    clazz2.getMethod("sayHello", String.class, String.class).invoke(clazz2.newInstance(), "Mrs Wang", "Good Morning!");

    System.out.println();

    System.out.println("实例化后是否相等:" + clazz1.equals(clazz2));
}

输出结果

输出结果为

JavaLibrary1.jar     Hello,Sid Lau!

JavaLibrary2.jar     Hello,Mrs Wang!Good Morning!

实例化后是否相等:false

OK,好了,我的目的达到了。

如果觉得这对你有用,请随意赞赏,给与作者支持
评论 2
最新评论
#1 楼 谦谦君子 2018-09-03

回复@amy 说:嗯,如果是简单的场景,这样处理也是可以的。但是我这太复杂了,好几个组件依赖的开源库都是上百的,大量的 log io 网络库都是冲突的,没办法,只能动态的去加载隔离。

#2 楼 amy 2018-09-03

我之前遇到这种冲突的情况,采取的是粗暴的方式,把其中一个jar整个包结构外再包装一层。学习了!