如果提前知道哪些类必须修改,那么修改类的最简单的方法如下:

  1. 通过调用ClassPool.get()获取一个CtClass对象,
  2. 修改它
  3. 在该CtClass对象上调用writeFile()或toBytecode()来获取修改的类文件。

如果在加载时才能确定一个类是否被修改,用户必须使Javassist与类加载器协作。Javassist可以与类加载器一起使用,以便在加载时可以修改字节码。 Javassist的用户可以定义自己的类加载器版本,但也可以使用Javassist提供的类加载器。

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()在Hello的say()方法中插入了一个println()方法,然后它在修改的Hello类上构造了一个实例,并在该实例上调用say()。

请注意,上面的程序取决于在调用toClass()之前,Hello类没有被加载的事实。如果不是,JVM会在toClass请求加载修改的Hello类之前加载原来的Hello类。 因此,加载修改的Hello类将失败(引发LinkageError)。例如,如果测试中的main()类似于此:

	public static void main(String[] args) throws Exception {
	    Hello orig = new Hello();
	    ClassPool cp = ClassPool.getDefault();
	    CtClass cc = cp.get("Hello");
	        :
	}

main方法的第一行就加载了原来的Hello类,则调用toClass会抛出异常,因为类加载器不会在同一时间加载两个不同版本的Hello类

如果程序在某些应用程序服务器(如JBoss和Tomcat)上运行,则toClass()使用的上下文类加载器可能不合适。在这种情况下,您会看到一个意外的ClassCastException。为了避免这种异常,你必须明确地给toClass()一个适当的类加载器。例如,如果bean是您的会话bean对象,那么以下代码会工作:

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

你应该给予toClass()已经加载程序的类加载器(在上面的例子中是bean对象的类)。

toClass()是为了方便提供的,如果您需要更复杂的功能,您应该编写自己的类加载器。

Java中的类加载

在Java中,多个类加载器可以共存,每个类加载器创建自己的命名空间。不同的类加载器可以加载具有相同类名的不同类文件。加载的两个类被认为是不同的。此功能使我们能够在单个JVM上运行多个应用程序,即使这些程序拥有相同名称但不同的类。

注意:JVM不允许动态重新加载类。 一旦类加载器加载类,它就不能在运行时重新加载该类的修改版本。 因此,在JVM加载它之后,您不能更改类的定义。 然而,JPDA(Java Platform Debugger Architecture)提供了重新加载类的有限能力。 见第3.6节。

如果同一个类文件由两个不同的类加载器加载,那么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类是不同的。

多个类加载器形成一个树结构。除了引导加载器之外,每个类加载器都有一个父类加载器,它通常加载该子类加载器的类。由于加载类的请求可以沿着类加载器的这个层次结构进行委派,所以类可能由你没有请求的类加载器加载。因此,已经请求加载类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加载。Window的启动器和实际加载器都是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加载.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.Loader可以被用来加载由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.Point。然后加载了这个修改的类,并创建了test.Rectangle的一个实例。

如果用户希望在加载类时按需修改它,则用户可以向javassist.Loader添加事件侦听器。添加的事件侦听器在类加载器加载类时收到通知。event-listener类必须实现以下接口:

	public interface Translator {
	    public void start(ClassPool pool)
	        throws NotFoundException, CannotCompileException;
	    public void onLoad(ClassPool pool, String classname)
	        throws NotFoundException, CannotCompileException;
	}

当javaxist.Loader中的addTranslator()将此事件侦听器添加到javassist.Loader对象时,将调用start()方法。在javassist.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);
	    }
	}

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

要使用MyTranslator对象运行应用程序类MyApp,请编写如下主要类:

	import javassist.*;
	
	public class Main2 {
	  public static void main(String[] args) throws Throwable {
	     Translator t = new MyTranslator();
	     ClassPool pool = ClassPool.getDefault();
	     Loader cl = new Loader();
	     cl.addTranslator(pool, t);
	     cl.run("MyApp", args);
	  }
	}

要运行这段程序,执行

	% java Main2 arg1 arg2...

MyApp类和其他应用程序类由MyTranslator转译

请注意,像MyApp这样的应用程序类无法访问诸如Main2,MyTranslator和ClassPool这些被加载的类,因为它们由不同的加载器加载。应用程序类由javassist.Loader加载,而被加载的类(如Main2)则由默认的Java类加载器加载。

javassist.Loader以与java.lang.ClassLoader不同的顺序搜索类。 ClassLoader首先将加载操作委托给父类加载器,然后仅在父类加载器找不到它们时尝试加载类。 另一方面,javassist.Loader尝试在委派给父类加载器之前加载这些类。 它仅在以下情况下进行委托:

  1. 通过在ClassPool对象上调用get()找不到类时
  2. 或者这些类通过使用delegateLoadingOf()来指定父类加载器加载。

此搜索顺序允许通过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();
	        }
	    }
	}

MyApp类是一个应用程序。要执行此程序,首先将类文件放在./class目录下,不能包含在类搜索路径中。否则,MyApp.class将被默认的系统类加载器加载,该加载器是SampleLoader的父加载器。目录名./class由构造函数中的insertClassPath()指定。如果需要,您可以选择不同的名称,而不是./class。然后做如下:

	% java SampleLoader

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

这是使用Javassist最简单的方法。但是,如果编写更复杂的类加载器,则可能需要详细了解Java类的加载机制。例如,上述程序将MyApp类置于与SampleLoader类所属的名称空间分开的名称空间中,因为这两个类由不同的类加载器加载。因此,MyApp类不能直接访问SampleLoader类。

修改系统类

像java.lang.String这样的系统类不能由系统类加载器以外的类加载器加载。 因此,上面显示的SampleLoader或javassist.Loader无法在加载时修改系统类。

如果您的应用程序需要这样做,系统类必须被静态修改。例如,以下程序向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 Runtime Environment二进制代码许可证。

运行时重加载类

如果JVM启动时启用了JPDA(Java Platform Debugger Architecture),则一个类是动态可重新加载的。在JVM加载一个类之后,可以卸载类定义的旧版本,并重新加载一个新的类定义。也就是说,该类的定义可以在运行时动态修改。但是,新的类定义必须与旧的定义有些兼容。JVM不允许在两个版本之间进行模式更改。他们有一套相同的方法和字段。

Javassist提供了一个方便的类,用于在运行时重新加载类。有关更多信息,请参阅javassist.tools.HotSwapper的API文档。