笔者所工作的一个项目需要使用动态调用方面的技术,这里说的“动态调用”不是普通
的Class.forName(String className),因为这种最常用的调用方式存在一个问题,那就是一旦在调用程
序中加载了class文件,如果你要再修改它并使之立刻生效的话是不可能的。我们需要解决这个问题,找
到可以替代的办法。
于是笔者在这方面做了一些探索性的工作,并做了一些调查,就目前了解的情况,在Java领域
有两种比较好的解决方案:一种是脚本技术,像Groovy、Beanshell、Python 、Javascript之类;另外一
种就是借助自定义的ClassLoader来实现。先说脚本,笔者在一些项目已经使用过这种技术,总得来说,
脚本语言可以在很多应用场景帮助我们解决问题,对于本文所要求的动态调用,它也是完全可以胜任的,
不过脚本技术最大的缺陷就是不易于调试和查错;我们知道,Java源文件在编译为class文件的时
候,Javac会帮助我们查找出程序语法错误,另外我们可以随时测试Class的方法,而脚本语言就不一样,
它没有这个过程,脚本直接被其引擎执行,并且我们不能对其做单元测试,另外它所代表的业务也是完全
暴露的。正因为脚本技术有上述缺陷,所以在我们的应用中,如果动态调用case比较多,动态调用中被调
用代码量很大而且与业务结合的比较紧密的话,笔者建议不要使用脚本来完成动态调用,开发和调试的工
作量非常大,而且运行也不一定高效。笔者在另外一篇文章中提到了脚本技术的应用,读者有兴趣可以去
看看。
上面说了一大堆没有营养的话,主角可以登场了,正如标题所说,我们现在要探讨的是Java
ClassLoader(后面用CL简称)是否可以帮助我们解决动态调用的问题。笔者在这里就不用说CL的概念,网
上有太多这方面的文章,我们可以检索。JVM中可能存在多个CL,每个CL拥有自己的NameSpace,CL之间有
父子关系,一个CL只能拥有一个class对象类型的实例,但是不同的CL可能拥有相同的class对象实例;CL
还有个特点,就是一旦父级CL已经load了某个Class,那么子CL是不能再load该CL的,我相信这个特性给很
多开发者已经制造了很多困扰,特别是基于APP server的开发。我们就以Tomcat为例说明一下,
Tomcat Server的ClassLoader结构如下:
+-----------------------------+
| Bootstrap |
| | |
| System |
| | |
| Common |
| / \ |
| Catalina Shared |
| / \ |
| WebApp1 WebApp2 |
+-----------------------------+
我们开发的web应用是WebApp2,我们需要版本为1.3的xml-apis.jar,于是我们把它放
在$TOMCAT_HOME$\webapps\WebApp2\WEB-INF\lib目录下,但是tomcat安装的时候,已经放置了一个版本
为1.2的xml-apis.jar,因为Common CL是WebApp2 CL的爷爷,所以爷爷权利大一点,于是WebApp2就只能使
用1.2的XML API了,与它需要的1.3不匹配,矛盾就产生了,WebApp2运行就会有问题。虽然这与本文主题
关系不大,在这里谈谈主要是让我们注意这种关系,在开发自定义的CL的时候可以考虑到这些问题。
好,现在我们开始正式“手工”探索,我们先写一个简单的Class,然后用一个自定义的CL来引导
它。切记,这个测试的Class不能被运行程序找到,即不能放入到运行程序的ClassPath中,否则测试没有
意义,如果放入到运行程序的ClassPath中,那么就会被自定义的CL的父CL所引导。
Class(Test1)的源代码如下:
package com.test;
import java.util.*;
public class Test1
{
public static String STR_CONSTANT = "good";
public Test1()
{
System.out.println("**********constructor
Test1,*****************:"+STR_CONSTANT);
}
}
CL(Test1ClassLoader)的源代码如下:
import java.io.*;
/*
* author:Jean(lxjchengcu@gmail.com)
*/
public class Test1ClassLoader extends ClassLoader {
private ClassLoader _parent;
public Test1ClassLoader(ClassLoader parent) {
_parent = parent;
}
// get bytes from file
private byte[] getBytes(String filename) throws IOException {
// get file size
File file = new File(filename);
long len = file.length();
byte raw[] = new byte[(int) len];
// open file
FileInputStream fin = new FileInputStream(file);
int r = fin.read(raw);
if (r != len)
throw new IOException("Can't read all, " + r + " != " + len);
fin.close();
return raw;
}
public synchronized Class loadClass(String name)
throws ClassNotFoundException
{
return loadClass(name,false);
}
// load class
public synchronized Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
Class clas = null;
clas = findLoadedClass(name);
if (clas == null) {
//System.out.println("not find loaded class");
}
if (clas == null) {
try {
clas = _parent.loadClass(name);
} catch (Exception e) {
//e.printStackTrace();
}
}
if (clas == null)
{
String fileStub = name.replace('.', '/');
String classFilename = fileStub + ".class";
try {
byte raw[] = getBytes("D:/test/" + classFilename);
clas = defineClass(name, raw, 0, raw.length);
System.out.println("define class name is "+name);
} catch (IOException ie) {
}
}
if (clas == null) {
clas = findSystemClass(name);
}
if (resolve && clas != null)
{
resolveClass(clas);
}
if (clas == null)
throw new ClassNotFoundException(name);
return clas;
}
public static void main(String args[])
throws Exception
{
Test1ClassLoader test1CL = new Test1ClassLoader(Test1ClassLoader.class
.getClassLoader());
Class cls = Class.forName("com.test.Test1",true,test1CL);
cls.newInstance();
}
}
运行CL,控制台输出如下:
define class name is com.test.Test1
**********constructor Test1,*****************:good
这证明Test1已经被Test1ClassLoader引导。读者可能有疑问,为什么这里构造CL,需要设置父CL呢,这
是因为运行所依赖的Class需要父级以上的CL引导,使用-verbose:class运行参数我们就可以从控制台输出
清楚地得出所有Class的装载过程。
现在我们要做测试是在程序运行期间修改Test1的内容,然后看CL能否引导最新的Class,为了完
成这个测试,我们需要调整Test1ClassLoader的main方法的代码:
public static void main(String args[])
throws Exception
{
Test1ClassLoader test1CL = new Test1ClassLoader(Test1ClassLoader.class
.getClassLoader());
while(true)
{
Class cls = Class.forName("com.test.Test1",true,test1CL);
cls.newInstance();
Thread.sleep(5000);
System.out.println("*******************************sleep
over*******************************");
}
}
在Thead sleep期间修改Test1的static变量STR_CONSTANT为bad,然后看输出结果:
define class name is com.test.Test1
**********constructor Test1,*****************:bad
*******************************sleep over*******************************
**********constructor Test1,*****************:bad
*******************************sleep over*******************************
**********constructor Test1,*****************:bad
怎么回事?我们需要的结果是:
define class name is com.test.Test1
**********constructor Test1,*****************:bad
*******************************sleep over*******************************
**********constructor Test1,*****************:good
*******************************sleep over*******************************
**********constructor Test1,*****************:good
这是因为cls已经缓存在起来了,所以每次loadClass的时候,就会先从缓存中读取出来,而不是从磁盘上
,既然这样,那我们换一个CL试试:
public static void main(String args[])
throws Exception
{
Test1ClassLoader test1CL = null;
while(true)
{
test1CL = new Test1ClassLoader(Test1ClassLoader.class
.getClassLoader());
Class cls = Class.forName("com.test.Test1",true,test1CL);
cls.newInstance();
Thread.sleep(5000);
System.out.println("*******************************sleep
over*******************************");
}
}
重新运行,在Thead sleep期间修改Test1的static变量STR_CONSTANT为bad,然后看输出结果:
define class name is com.test.Test1
**********constructor Test1,*****************:good
*******************************sleep over*******************************
define class name is com.test.Test1
**********constructor Test1,*****************:bad
*******************************sleep over*******************************
define class name is com.test.Test1
**********constructor Test1,*****************:bad
*******************************sleep over*******************************
好,成功完成既定任务,也就是说我们需要使用另一个CL来引导更改的Class(当然,还可以有其他办法
,比如我们loadClass的时候发现name=Test1,就不从缓存中获取Class,而是读stream来构造Class实例,但
笔者认为这种hardCode方法不妥也不通用)。