如果在加载时确定类是否被修改,用户必须让Javassist与类加载器协作。Javassist可以与类加载器一起使用,这样就可以在加载时修改字节码。Javassist的用户可以定义自己版本的类加载器,但也可以使用Javassist提供的类加载器。
CtClass提供了一个方便的方法toClass(),它请求当前线程的上下文类加载器来装入由CtClass对象表示的类。要调用此方法,调用方必须具有适当的权限;否则可能抛出SecurityException。下面的程序展示了如何使用toClass():
public class Hello {
public void say() {
System.out.println("Hello");
}
}
public class Test {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
CtMethod m = cc.getDeclaredMethod("say");
m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
}
Test.main()在Hello中的say()方法体中插入对println()的调用。然后它构造一个修改后的Hello类的实例,并在该实例上调用say()。
注意,上面的程序依赖于在调用toClass()之前从未加载Hello类这一事实。如果不是,JVM将在toClass()请求加载修改后的Hello类之前加载原始的Hello类。因此加载修改后的Hello类将失败(引发LinkageError)。例如,如果Test中的main()是这样的:
public static void main(String[] args) throws Exception {
Hello orig = new Hello();
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("Hello");
:
}
那么原来的Hello类在main的第一行被加载,对toClass()的调用会抛出一个异常,因为类加载器不能同时加载两个不同版本的Hello类。
如果程序运行在JBoss和Tomcat等应用服务器上,那么toClass()使用的上下文类加载器可能不合适。在这种情况下,您将看到一个意外的ClassCastException。为了避免这种异常,必须显式地为toClass()提供适当的类加载器。例如,如果bean是会话bean对象,那么以下代码:
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
你应该给class()一个已经加载了你的程序的类加载器。提供toClass()是为了方便。如果您需要更复杂的功能,您应该编写自己的类加载器。
在Java中,多个类加载器可以共存,每个类加载器创建自己的名称空间。不同的类加载器可以装入具有相同类名的不同类文件。加载的两个类视为不同的类。这个特性使我们可以在同一个JVM中运行多个应用即使这个应用程序包含具有相同名字的不同的类。
注意: JVM不允许动态地重新加载一个类。类加载器加载类后,就不能在运行时期间加载该类的修改版本。因此,您不能在JVM加载类之后更改类的定义。然而,JPDA (Java平台调试器体系结构)为重新加载类提供了有限的能力。
如果相同的类文件由两个不同的类加载器器加载,JVM将生成两个具有相同名称和定义的不同类。这两个类被看作是不同的类。由于这两个类不相同,一个类的实例不能赋值给另一个类的变量。两个类之间的强制转换操作失败并抛出ClassCastException。
例如,下面的代码片段会抛出一个异常:
MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj; // this always throws ClassCastException.
Box类由两个类加载器装入。假设类加载器CL装入包含此代码段的类。由于这个代码片段引用了MyClassLoader、Class、Object和Box, CL也加载了这些类(除非它委托给另一个类加载器)。因此变量b的类型是CL加载的Box类。另一方面,myLoader也加载Box类。对象obj是myLoader加载的Box类的实例。因此,最后一条语句总是抛出一个ClassCastException,因为obj的类与用作th的Box类是不同版本的。
多个类加载器呈现树形结构。除引导类加载器外,每个类加载器都有一个父类加载器,父类加载器通常装入该子类加载器的类。由于加载器类的请求可以沿着类加载器的这个层次结构进行委托,因此类可以由您没有请求类加载的类加载器装入。因此,被请求装入类C的类加载器可能与实际加载类C的类加载器不同。为了区别,我们称前一个加载器为C的启动器,称后一个加载器为C的真正加载器。
此外,如果类加载器CL请求装入类C (C的启动器),则委托给父类加载器PL,则类加载器CL永远不会被请求装入类C定义中引用的任何类。CL不是这些类的启动器。相反,父类加载器PL成为它们的启动器,并请求装入它们。类C的定义所引用的类是由C的真正加载器装入的。
为了理解这种行为,让我们考虑下面的例子。
public class Point { // loaded by PL
private int x, y;
public int getX() { return x; }
:
}
public class Box { // the initiator is L but the real loader is PL
private Point upperLeft, size;
public int getBaseX() { return upperLeft.x; }
:
}
public class Window { // loaded by a class loader L
private Box box;
public int getBaseX() { return box.getBaseX(); }
}
假设一个类Window是由类加载器L加载的,windows的启动器和真正的加载器都是L。因为Window的定义是指Box,所以JVM会请求L加载Box。在这里,假设L将这个任务委托给父类加载器PL。Box的启动器是L,但真正的加载器是PL。在这种情况下,Point的启动器不是L,而是PL,因为它与Box的真实加载器相同。因此,L从未被要求加载Point。
接下来,让我们考虑一个稍作修改的示例。
public class Point {
private int x, y;
public int getX() { return x; }
:
}
public class Box { // the initiator is L but the real loader is PL
private Point upperLeft, size;
public Point getSize() { return size; }
:
}
public class Window { // loaded by a class loader L
private Box box;
public boolean widthIs(int w) {
Point p = box.getSize();
return w == p.getX();
}
}
Window的定义也指的是Point。在这种情况下,如果类加载器L被请求装入Point,它也必须委托给PL。您必须避免让两个类加载器双重装入同一个类
如果在加载Point时L没有委托给PL, widthIs()将抛出ClassCastException。由于Box的实际加载器是PL,因此Box中引用的Point也是PL加载的。因此getSize()的结果值是PL加载的Point的实例,而widthIs()中变量p的类型是l加载的Point。JVM将它们视为不同的类型,因此由于类型不匹配而抛出异常。
这种行为有点不方便,但却是必要的。如果有以下陈述:
Point p = box.getSize();
没有抛出异常,那么Window的程序员可能会破坏Point对象的封装。例如,在PL加载的Point中,x字段是私有的。然而,如果L用以下定义加载Point, Window类可以直接访问x的值:
public class Point {
public int x, y; // not private
public int getX() { return x; }
:
}
有关Java中类加载器的更多细节,请参考以下文章:
Sheng Liang and Gilad Bracha, “Dynamic Class Loading in the Java Virtual Machine”,
ACM OOPSLA’98, pp.36-44, 1998.
Javassist提供了一个类加载器Javassist . loader。这个类加载器使用javassist。用于读取类文件的ClassPool对象。
例如,javassist加载器可用于加载用Javassist修改过的特定类。
import javassist.*;
import test.Rectangle;
public class Main {
public static void main(String[] args) throws Throwable {
ClassPool pool = ClassPool.getDefault();
Loader cl = new Loader(pool);
CtClass ct = pool.get("test.Rectangle");
ct.setSuperclass(pool.get("test.Point"));
Class c = cl.loadClass("test.Rectangle");
Object rect = c.newInstance();
:
}
}
这个程序修改了一个类test.Rectangle。test.Rectangle的超类设置为test.Point。然后这个程序加载修改后的类,并创建。test.Rectangle的新实例。
如果用户希望在加载类时按需修改类,则可以向javassist.Loader添加事件侦听器。当类加载器装入类时,将通知添加的事件侦听器。事件监听器类必须实现以下接口:
public interface Translator {
public void start(ClassPool pool)
throws NotFoundException, CannotCompileException;
public void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException;
}
当事件侦听器添加到javassist时,方法start()被调用。当Loader装入一个类时方法OnLoad被调用。onLoad()可以修改所加载类的定义。
例如,下面的事件侦听器在加载所有类之前将它们更改为公共类。
public class MyTranslator implements Translator {
void start(ClassPool pool)
throws NotFoundException, CannotCompileException {}
void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException
{
CtClass cc = pool.get(classname);
cc.setModifiers(Modifier.PUBLIC);
}
}
javassist.Loader搜索类的顺序与java.lang.ClassLoader不同。ClassLoader首先将装入操作委托给父类加载器,然后仅在父类加载器找不到类时才尝试装入这些类。
javassist.Loader尝试在委托给父类加载器之前加载类。它仅在以下情况下进行委托:
这个搜索顺序允许通过Javassist加载修改过的类。但是,如果由于某种原因无法找到修改过的类,它将委托给父类加载器。一旦一个类被父类加载器装入,该类中引用的其他类也将被父类加载器装入,因此它们永远不会被修改。回想一下,类C中引用的所有类都是由C的实际加载器加载的。如果您的程序无法加载一个修改过的类,那么您应该确保使用该类的所有类是否都已由javassist.Loader加载。
使用Javassist的简单类加载器如下所示:
import javassist.*;
public class SampleLoader extends ClassLoader {
/* Call MyApp.main().
*/
public static void main(String[] args) throws Throwable {
SampleLoader s = new SampleLoader();
Class c = s.loadClass("MyApp");
c.getDeclaredMethod("main", new Class[] { String[].class })
.invoke(null, new Object[] { args });
}
private ClassPool pool;
public SampleLoader() throws NotFoundException {
pool = new ClassPool();
pool.insertClassPath("./class"); // MyApp.class must be there.
}
/* Finds a specified class.
* The bytecode for that class can be modified.
*/
protected Class findClass(String name) throws ClassNotFoundException {
try {
CtClass cc = pool.get(name);
// modify the CtClass object here
byte[] b = cc.toBytecode();
return defineClass(name, b, 0, b.length);
} catch (NotFoundException e) {
throw new ClassNotFoundException();
} catch (IOException e) {
throw new ClassNotFoundException();
} catch (CannotCompileException e) {
throw new ClassNotFoundException();
}
}
}
% java SampleLoader
类加载器装入类MyApp (./class/MyApp.class),并使用命令行参数调用MyApp.main()
这是使用Javassist的最简单方法。但是,如果要编写更复杂的类加载器,则可能需要详细了解Java的类加载机制。例如,上面的程序将MyApp类放在与SampleLoader类所属的名称空间分离的名称空间中,因为这两个类是由不同的类加载器装入的。因此,MyApp类不能直接访问类SampleLoader。
像java.lang.String这样的系统类不能由系统类加载器以外的类加载器装入。因此SampleLoader或javassist上面显示的加载器不能在加载时修改系统类。
如果应用程序需要这样做,则必须静态修改系统类。例如,下面的程序在java.lang.String中添加了一个新的字段hiddenValue:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("java.lang.String");
CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
f.setModifiers(Modifier.PUBLIC);
cc.addField(f);
cc.writeFile(".");
这个程序生成一个文件"./java/lang/String.class"。
要使用这个修改后的String类运行你的程序MyApp,请执行以下操作:
% java -Xbootclasspath/p:. MyApp arg1 arg2...
假设MyApp的定义如下:
public class MyApp {
public static void main(String[] args) throws Exception {
System.out.println(String.class.getField("hiddenValue").getName());
}
}
如果修改后的String类被正确加载,MyApp打印hiddenValue。
注意:不应该部署使用这种技术来覆盖rt.jar中的系统类的应用程序,因为这样做会违反Java 2运行时环境二进制代码许可。
如果JVM启动时启用了JPDA (Java平台调试器体系结构),则可以动态地重新加载类。JVM加载类之后,可以卸载旧版本的类定义,并重新加载新版本的类定义。也就是说,该类的定义可以在运行时期间动态修改。但是,新的类定义必须在一定程度上与旧的类定义兼容。JVM不允许在两个版本之间更改模式。它们具有相同的方法和字段集。
Javassist为在运行时重新加载类提供了一个方便的类。有关更多信息,请参阅javassist.tools.HotSwapper的API文档。
[资料来源] http://www.javassist.org/tutorial/tutorial.html