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

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

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

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

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

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

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

还有一个情况是,现在 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.jarJavaLibrary2.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整个包结构外再包装一层。学习了!