如果需要修改的类是预先知道的,最简单的修改方式如下:
- 调用ClassPool.get()获取CtClass对象
- 修改
- 调用CtClass对象的writeFile()或者toBytecode()来获取已修改的类文件
如果在加载时不确定类是否已被修改,用户一定要配合Classloader来使用Javassist。Javassist可以和Classloader一起使用,这样就可以在加载时修改字节码。Javassist的使用者可以定义自己的Classloader,也可以使用Javassist提供的Classloader。
1.CtClass的toClass方法
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()在方法say()的方法体中插入了一个println()调用。然后它构建了一个修改后的Hello类的实例并调用了say()方法。
注意上面这段程序能运行的前提是Hello类在toClass()被调用前没有被加载。如果不是,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是你的会话对象,那么下面的代码:
CtClass cc = ...; Class c = cc.toClass(bean.getClass().getClassLoader());
可以工作。你应该将加载你程序(在上面的例子中,指的是bean对象的类)的类加载器传给toClass()。
toClass()提供了一种方便的方式。如果你需要更复杂的功能,你应该写一个你自己的类加载器。
2. Java的类加载
在Java中,多个类加载器同时存在,每一个加载器创建一个自己的命名空间。不同的类加载器可以加载类名相同的不同类文件。被加载的两个类被视为不同的类。这个特性允许我们在一个JVM上运行多个应用程序,即使程序包含多个类名相同的类。
**注意:**JVM不允许动态重载类。一旦类加载一个类,它在运行期间不能重新加载一个修改过的类。因此,你不能在JVM加载类之后再修改类的定义。然而JPDA(Java Platform Debugger Architecture)提供了有限的类重载。
如果相同的类文件被两个不同的类加载器加载,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的类型和变量b的类型不是一个版本的Box类。
多个类加载器形成了一课构造书。除了bootstrap loader,每一个类加载器都拥有一个父加载器,它可以加载子类加载器的类。因为加载类的请求可以被委托给这个层次的类加载器,所以一个类可能会被你没有请求的类加载器加载。因此,请求加载类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加载。发起者和实际加载器都是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也必须委托PL,如果它被请求去加载Point。你必须避免两个类加载器同时加载同一个类。两个加载中的一个必须委托给另一个。
如果当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是私有的。然而,类Window可以直接访问x的变量,如果L用下面的定义加载Point:
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.