[toc]

如果一开始你就知道要修改哪个类,那么最简单的方式如下:

  • 1.调用ClassPool.get() 来获取一个CtClass对象。
  • 2.修改它
  • 3.调用writeFile()toBytecode() 来获取一个修改后的class文件

如果一个类是否要被修改是在加载时确定的,用户就必须让Javassist和类加载器协作。Javassist可以和类加载器一块儿使用,以便于可以在加载时修改字节码。用户可以自定义类加载器,也可以使用Javassist提供好的。

3.1. CtClass的 toClass() 方法

CtClass提供了一个方便的方法toClass(), 它会请求当前线程的上下文类加载器,让其加载CtClass对象所代表的那个类。要调用这个方法,必须要拥有权限。此外,该方法还会抛出SecurityException异常。

使用toClass() 方法样例:

  1. public class Hello {
  2. public void say() {
  3. System.out.println("Hello");
  4. }
  5. }
  6. public class Test {
  7. public static void main(String[] args) throws Exception {
  8. ClassPool cp = ClassPool.getDefault();
  9. CtClass cc = cp.get("Hello");
  10. // 获取say方法
  11. CtMethod m = cc.getDeclaredMethod("say");
  12. // 在方法第一行前面插入代码
  13. m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
  14. Class c = cc.toClass();
  15. Hello h = (Hello)c.newInstance();
  16. h.say();
  17. }
  18. }

Test.main()Hellosay() 方法的方法体中插入了println() 的调用。然后构建了被修改后的Hello的实例,然后调用了该实例的say() 方法。

注意,上面这段程序有一个前提,就是Hello类在调用toClass() 之前没有被加载过。否则,在toClass() 请求加载被修改后的Hello类之前,JVM就会加载原始的Hello类。因此,加载被修改后的Hello类就会失败(抛出LinkageError)。例如:

  1. public static void main(String[] args) throws Exception {
  2. Hello orig = new Hello();
  3. ClassPool cp = ClassPool.getDefault();
  4. CtClass cc = cp.get("Hello");
  5. Class c = cc.toClass(); // 这句会报错
  6. }

main函数的第一行加载了Hello类,cc.toClass() 这行就会抛出异常。原因是类加载器不能同时加载两个不同版本的Hello类。

如果你的程序运行在JBOSS或Tomcat的应用服务器上,那么你再用toClass() 就有点不合适了。这种情况下,将会抛出ClassCastException异常。为了避免这个异常,你必须给toClass() 一个合适的类加载器。例如,假设bean是你的会话bean对象,那么这段代码:

  1. CtClass cc = ...;
  2. Class c = cc.toClass(bean.getClass().getClassLoader());

这段代码可以正常运行。你应该给toClass() 的类加载器是加载你程序的加载器(上面的例子中,就是bean对象的class的类加载器)。

toClass() 已经很方便了。你要是想更复杂的类加载器,你应该自定义类加载器。

3.2 Java中的类加载

在Java中,多个类加载器可以共存,它们可以创建自己的命名空间。不同的类加载器能够加载有着相同类名的不同的类文件。被加载过的两个类会被视为不同的东西。这个特点可以让我们在一个JVM中运行多个应用程序,尽管它们包含了有着相同名称的不同的类。

JVM不允许动态重新加载一个类。一旦类加载加载过一个类之后,在运行期就不能在加载该类的另一个被修改过的版本。因此,你不能在JVM加载过一个类之后修改它的定义。但是,JPDA(Java Platform Debugger Architecture)提供了重新加载类的一些能力。详细请看3.6

如果两个不同的类加载器加载里一个相同的Class文件,那么JVM会生成两个不同的Class,虽然它们拥有相同的名字和定义。这两个Class会被视为两个不同的东西。因为这两个Class不是完全相同的,所以一个Class的实例不能赋值给另一个Class的变量。这两个类之间的类型转换会失败,抛出ClassCastException异常。

例如,下面这个代码片段就会抛出该异常:

  1. MyClassLoader myLoader = new MyClassLoader();
  2. Class clazz = myLoader.loadClass("Box");
  3. Object obj = clazz.newInstance();
  4. Box b = (Box)obj; // 这里总是会抛出ClassCastException异常.

Box类被两个类加载器所加载。假定CL类加载器加载了这段代码片段。因为该代码中引用了MyClassLoader,Class,Object,所以CL也会加载这些类(除非它代理了其它啊类加载器)。因此,b 变量的类型是CL加载的Box。但是obj变量的类型是myLoader加载的Box,虽然都是Box,但是不一样。所以,最后一段代码一定会抛出ClassCastException,因为bobj是两个不同版本的Box

多个类加载形成了一个树型结构。除了启动加载器之外,其他的类加载器都有一个父类加载,子类加载器通常由父类加载器加载。由于加载类的请求可以沿着加载器的层次结构进行委托,所以你请求加载类的加载器,并不一定真的是由这个加载器加载的,也可能换其他加载器加载了。因此(举例),请求加载类C的加载器可能不是真正加载类C的加载器。不同的是,我们将前面的加载器称为C的发起者(initiator),后面的加载器称为C实际的加载器(real loader)。

除此之外,如果类加载器CL请求加载一个类C(C的发起者)委托给了它的父类加载器PL,那么类加载器CL也不会加载类C定义中引用的任何其他类。对于那些类,CL不是它们的发起者,相反,父加载器PL则会称为它们的发起者,并且回去加载它们。类C定义中引用的类,由类C的实际的加载器去加载。

要理解上面的行为,可以参考下面代码:

  1. public class Point { // PL加载该类
  2. private int x, y;
  3. public int getX() { return x; }
  4. :
  5. }
  6. public class Box { // L是发起者,但实际的加载器是PL
  7. private Point upperLeft, size;
  8. public int getBaseX() { return upperLeft.x; }
  9. :
  10. }
  11. public class Window { // 该类被加载器L加载
  12. private Box box;
  13. public int getBaseX() { return box.getBaseX(); }
  14. }

假定类加载器L加载Window类。加载Window的发起者和实际加载者都是L。因为Window的定义引用了类Box,所以JVM会让 L去加载Box类。这里,假定L将该任务委托给了父类加载器PL,所以加载Box的发起者是L,但实际加载者是PL。这种情况下,PL作为Box的实际加载者,就会去加载Box中定义中引用的Point类,所以Point的发起者和实际加载者都是PL。因此加载器L从来都没有请求过加载Point类。

把上面的例子稍微改一下:

  1. public class Point {
  2. private int x, y;
  3. public int getX() { return x; }
  4. :
  5. }
  6. public class Box { // 发起者是L,但实际加载者是PL
  7. private Point upperLeft, size;
  8. public Point getSize() { return size; }
  9. :
  10. }
  11. public class Window { // Window由加载器L加载
  12. private Box box;
  13. public boolean widthIs(int w) { // 增加了方法,方法中有对Point类的引用。
  14. Point p = box.getSize();
  15. return w == p.getX();
  16. }
  17. }

上面中,Window也引用了Point。这样,如果加载器L需要加载Point的话,L也必须委托给PL你必须避免让两个类加载器重复加载同一个类。两个加载器中的一个必须委托给另一个加载器。

如果当Point被加载时,L没有委托给PL,那么widthIs()就会抛出ClassCastException。因为Window里的PointL加载的,而Box中的PointPL加载器加载的。你用box.getSize() 返回的PL.PointL的Point,那么就会JVM就会认为它们是不同的实例,进而抛出异常。

这样有些不方便,但是需要有这种限制。比如:

  1. Point p = box.getSize();

如果这条语句没有抛出异常,那么Window的代码就有可能打破Point的封装。例如,PL加载的Pointx变量是private,但是L加载器加载的Pointx变量是public(下面的代码定义),那么不就打破了封装定义。

  1. public class Point {
  2. public int x, y; // not private
  3. public int getX() { return x; }
  4. }

要是想了解更多关于JAVA类加载器的细节,可以参考下面这个论文:

  1. Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine",
  2. ACM OOPSLA'98, pp.36-44, 1998.

3.3 使用javassist.Loader

Javassist提供了一个类加载器javasist.Loader,该加载器使用一个javassist.ClassPool对象来读取类文件。

例如,javassist.Loader可以加载一个被Javassist修改过的特定类:

  1. import javassist.*;
  2. import test.Rectangle;
  3. public class Main {
  4. public static void main(String[] args) throws Throwable {
  5. ClassPool pool = ClassPool.getDefault();
  6. Loader cl = new Loader(pool);
  7. CtClass ct = pool.get("test.Rectangle");
  8. ct.setSuperclass(pool.get("test.Point"));
  9. Class c = cl.loadClass("test.Rectangle");
  10. Object rect = c.newInstance();
  11. }
  12. }

这段 程序修改了test.Rectangle,将它的父类设置为了test.Point。然后程序加载了修改后的类,并且创建了test.Rectangle的一个新实例。

如果用户想根据需要在类被加载的时候修改类,那么用户可以增添一个事件监听器给javassist.Loader。该事件监听器会在类加载器加载类时被通知。事件监听器必须实现下面这个接口:

  1. public interface Translator {
  2. public void start(ClassPool pool)
  3. throws NotFoundException, CannotCompileException;
  4. public void onLoad(ClassPool pool, String classname)
  5. throws NotFoundException, CannotCompileException;
  6. }

当使用javassist.LoaderaddTranslator() 方法增添事件监听器时,start() 方法就会被调用。在javassist.Loader加载类之前,onLoad() 方法就会被调用。你可以在onLoad() 方法中修改要加载的类的定义。

例如,下面的事件监听器就在类被加载之前把它们都修改成public类。

  1. public class MyTranslator implements Translator {
  2. void start(ClassPool pool)
  3. throws NotFoundException, CannotCompileException {}
  4. void onLoad(ClassPool pool, String classname)
  5. throws NotFoundException, CannotCompileException
  6. {
  7. CtClass cc = pool.get(classname);
  8. cc.setModifiers(Modifier.PUBLIC);
  9. }
  10. }

注意onLoad()不必调用toBytecode()writeFile(),因为javassist.Loader会调用这些方法来获取类文件。

要想运行一个带有Mytranslator对象的application(带main方法,可以运行的)类MyApp,可以这样写:

  1. import javassist.*;
  2. public class Main2 {
  3. public static void main(String[] args) throws Throwable {
  4. Translator t = new MyTranslator();
  5. ClassPool pool = ClassPool.getDefault();
  6. Loader cl = new Loader();
  7. cl.addTranslator(pool, t);
  8. cl.run("MyApp", args);
  9. }
  10. }

然后这样运行这个程序:

  1. > java Main2 arg1 arg2...

这样MyApp和其他的应用程序类就会被MyTranslator转换了。

注意,像MyApp这样的应用类不能访问加载器的类,不如Main2MyTranslatorClassPool。因为他们是被不同的加载器加载的。应用类时javassist.Loader加载的,然而像Main2这些是被默认的java类加载器加载的。

javassist.Loader搜索类的顺序和java.lang.ClassLoader.ClassLoader不同。JavaClassLoader首先会委托父加载器进行加载操作,父加载器找不到的时候,才会由子加载器加载。而javassist.Loader首先尝试加载类,然后才会委托给父加载器。只有在下面这些情况才会进行委托:

  • 调用get()方法后在ClassPool对象中找不到
  • 使用delegateLoadingOf() 方法指定要父类加载器去加载

这个搜索顺序机制允许Javassist加载修改后的类。然而,如果它因为某些原因找不到修改后的类的话,就会委托父加载器去加载。一旦该类被父加载器加载,那么该类中引用的类也会用父加载器加载,并且它们不能再被修改了。回想下,之前类C的实际加载器加载了类C所有引用的类。如果你的程序加载一个修改过的类失败了,那么你就得想想是否那些类是否使用了被javassist.Loader加载的类。

3.4 自定义一个类加载器

一个简单的类加载器如下:

  1. import javassist.*;
  2. public class SampleLoader extends ClassLoader {
  3. /* Call MyApp.main().
  4. */
  5. public static void main(String[] args) throws Throwable {
  6. SampleLoader s = new SampleLoader();
  7. Class c = s.loadClass("MyApp");
  8. c.getDeclaredMethod("main", new Class[] { String[].class })
  9. .invoke(null, new Object[] { args });
  10. }
  11. private ClassPool pool;
  12. public SampleLoader() throws NotFoundException {
  13. pool = new ClassPool();
  14. pool.insertClassPath("./class"); // MyApp.class must be there.
  15. }
  16. /* Finds a specified class.
  17. * The bytecode for that class can be modified.
  18. */
  19. protected Class findClass(String name) throws ClassNotFoundException {
  20. try {
  21. CtClass cc = pool.get(name);
  22. // modify the CtClass object here
  23. byte[] b = cc.toBytecode();
  24. return defineClass(name, b, 0, b.length);
  25. } catch (NotFoundException e) {
  26. throw new ClassNotFoundException();
  27. } catch (IOException e) {
  28. throw new ClassNotFoundException();
  29. } catch (CannotCompileException e) {
  30. throw new ClassNotFoundException();
  31. }
  32. }
  33. }

MyApp是一个应用程序。要执行这段程序,首先要放一个class文件到 ./class 目录下,该目录不能包含在类搜索路径下。否则,MyApp.class将会被默认的系统类加载器加载,也就是SampleLoader的父类加载器。你也可以把insertClassPath中的 ./class 放入构造函数的参数中,这样你就可以选择自己想要的路径了。 运行java程序:

  1. > java SampleLoader

类加载器加载了类MyApp(./class/MyApp.class),并且调用了MyApp.main() ,并传入了命令行参数。

这是使用Javassist最简单的方式。如果你想写个更复杂的类加载器,你可能需要更多的java类加载机制的知识。例如,上面的程序把MyApp的命名空间和SampleLoader的命名空间是分开的,因为它们两个类是由不同的类加载器加载的。因此,MyApp不能直接访问SampleLoader类。

3.5 修改系统类

除了系统类加载器,系统类不能被其他加载器加载,比如java.lang.String。因此,上面的SampleLoaderjavassist.Loader在加载期间不能修改系统类。

如果你的程序非要那么做,请“静态的”修改系统类。例如,下面的程序给java.lang.String增添了hiddenValue属性。

  1. ClassPool pool = ClassPool.getDefault();
  2. CtClass cc = pool.get("java.lang.String");
  3. CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
  4. f.setModifiers(Modifier.PUBLIC);
  5. cc.addField(f);
  6. cc.writeFile(".");

这个程序会生成一个文件 ./java/lang/String.class

用修改过的String类运行一下你的程序MyApp,按照下面:

  1. > java -Xbootclasspath/p:. MyApp arg1 arg2...

假定MyApp的代码是这样的:

  1. public class MyApp {
  2. public static void main(String[] args) throws Exception {
  3. System.out.println(String.class.getField("hiddenValue").getName());
  4. }
  5. }

如果被修改的String正常加载的话,MyApp就会打印hiddenValue

应用最好不要使用该技术去重写rt.jar中的内容,这样会违反Java 2 Runtime Environment binary code 协议。

3.6 运行期重新加载一个类

启动JVM时,如果开启了JPDA(Java Platform Debugger Architecture),那么class就可以动态的重新加载了。在JVM加载一个类之后,旧的类可以被卸载,然后重新加载一个新版的类。意思就是,类的定义可以在运行期动态的修改。但是,新类一定要能和旧类相互兼容。JVM不允许两个版本存在模式的改变,它们需要有相同的方法和属性。

Javassist提供了一个很方便的类,用于在运行期改变类。想了解更多信息,可以看javassist.tools.HotSwapper的API文档