类加载器 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,好了,我的目的达到了。
回复@amy 说:嗯,如果是简单的场景,这样处理也是可以的。但是我这太复杂了,好几个组件依赖的开源库都是上百的,大量的 log io 网络库都是冲突的,没办法,只能动态的去加载隔离。
我之前遇到这种冲突的情况,采取的是粗暴的方式,把其中一个jar整个包结构外再包装一层。学习了!