Javassist 使用指南(一)

本文译自: Javassist Tutorial-1
原作者: Shigeru Chiba
完成时间:2016年11月

1. 读写字节码

我们知道 Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口。Javaassist 就是一个用来处理 Java 字节码的类库。

在 Javassist 中,类 Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,下面是一个简单的例子:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();

这段代码首先获取一个 ClassPool 对象。ClassPool 是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。

为了修改类的定义,首先需要使用 ClassPool.get() 方法来从 ClassPool 中获得一个 CtClass 对象。上面的代码中,我们从 ClassPool 中获得了代表 test.Rectangle 类的 CtClass 对象的引用,并将其赋值给变量 cc。使用 getDefault() 方法获取的 ClassPool 对象使用的是默认系统的类搜索路径。

从实现的角度来看,ClassPool 是一个存储 CtClass 的 Hash 表,类的名称作为 Hash 表的 key。ClassPool 的 get() 函数用于从 Hash 表中查找 key 对应的 CtClass 对象。如果没有找到,get() 函数会创建并返回一个新的 CtClass 对象,这个新对象会保存在 Hash 表中。

从 ClassPool 中获取的 CtClass 是可以被修改的(稍后会讨论细节)。

在上面的例子中,test.Rectangle 的父类被设置为 test.Point。调用 writeFile() 后,这项修改会被写入原始类文件。writeFile() 会将 CtClass 对象转换成类文件并写到本地磁盘。也可以使用 toBytecode() 函数来获取修改过的字节码:

byte[] b = cc.toBytecode();

你也可以通过 toClass() 函数直接将 CtClass 转换成 Class 对象:

Class clazz = cc.toClass();

toClass() 请求当前线程的 ClassLoader 加载 CtClass 所代表的类文件。它返回此类文件的 java.lang.Class 对象,更多细节,请参考下面的章节。

定义新类

使用 ClassPool 的 makeClass() 方法可以定义一个新类。

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");

这段代码定义了一个空的 Point 类。Point 类的成员方法可以通过 CtNewMethod 类的工厂方法来创建,然后使用 CtClass 的 addMethod() 方法将其添加到 Point 中。

使用 ClassPool 中的 makeInterface() 方法可以创建新接口。接口中的方法可以使用 CtNewMethod 的 abstractMethod() 方法创建。

将类冻结

如果一个 CtClass 对象通过 writeFile(), toClass(), toBytecode() 被转换成一个类文件,此 CtClass 对象会被冻结起来,不允许再修改。因为一个类只能被 JVM 加载一次。

但是,一个冷冻的 CtClass 也可以被解冻,例如:

CtClasss cc = ...;
    :
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // 因为类已经被解冻,所以这里可以调用成功

调用 defrost() 之后,此 CtClass 对象又可以被修改了。

如果 ClassPool.doPruning 被设置为 true,Javassist 在冻结 CtClass 时,会修剪 CtClass 的数据结构。为了减少内存的消耗,修剪操作会丢弃 CtClass 对象中不必要的属性。例如,Code_attribute 结构会被丢弃。一个 CtClass 对象被修改之后,方法的字节码是不可访问的,但是方法名称、方法签名、注解信息可以被访问。修剪过的 CtClass 对象不能再次被解冻。ClassPool.doPruning 的默认值为 false。

stopPruning() 可以用来驳回修剪操作。

CtClasss cc = ...;
cc.stopPruning(true);
    :
cc.writeFile(); // 转换成一个 class 文件
// cc is not pruned.

这个 CtClass 没有被修剪,所以在 writeFile() 之后,可以被解冻。

注意:调试的时候,你可能临时需要停止修剪和冻结,然后保存一个修改过的类文件到磁盘,debugWriteFile() 方法正是为此准备的。它停止修剪,然后写类文件,然后解冻并再次打开修剪(如果开始时修养是打开的)。

类搜索路径

通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。

下面的例子中,pool 代表一个 ClassPool 对象:

pool.insertClassPath(new ClassClassPath(this.getClass()));

上面的语句将 this 指向的类添加到 pool 的类加载路径中。你可以使用任意 Class 对象来代替 this.getClass(),从而将 Class 对象添加到类加载路径中。

也可以注册一个目录作为类搜索路径。下面的例子将 /usr/local/javalib 添加到类搜索路径中:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");

类搜索路径不但可以是目录,还可以是 URL :

ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);

上述代码将 http://www.javassist.org:80/java/ 添加到类搜索路径。并且这个URL只能搜索 org.javassist 包里面的类。例如,为了加载 org.javassist.test.Main,它的类文件会从获取 http://www.javassist.org:80/java/org/javassist/test/Main.class 获取。

此外,也可以直接传递一个 byte 数组给 ClassPool 来构造一个 CtClass 对象,完成这项操作,需要使用 ByteArrayPath 类。示例:

ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);

示例中的 CtClass 对象表示 b 代表的 class 文件。将对应的类名传递给 ClassPool 的 get() 方法,就可以从 ByteArrayClassPath 中读取到对应的类文件。

如果你不知道类的全名,可以使用 makeClass() 方法:

ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);

makeClass() 返回从给定输入流构造的 CtClass 对象。 你可以使用 makeClass() 将类文件提供给 ClassPool 对象。如果搜索路径包含大的 jar 文件,这可能会提高性能。由于 ClassPool 对象按需读取类文件,它可能会重复搜索整个 jar 文件中的每个类文件。 makeClass() 可以用于优化此搜索。由 makeClass() 构造的 CtClass 保存在 ClassPool 对象中,从而使得类文件不会再被读取。

用户可以通过实现 ClassPath 接口来扩展类加载路径,然后调用 ClassPool 的 insertClassPath() 方法将路径添加进来。这种技术主要用于将非标准资源添加到类搜索路径中。

2. ClassPool

ClassPool 是 CtClass 对象的容器。因为编译器在编译引用 CtClass 代表的 Java 类的源代码时,可能会引用 CtClass 对象,所以一旦一个 CtClass 被创建,它就被保存在 ClassPool 中.

例如,一个 CtClass 类代表 Point 类,并给 CtClass 添加 getter() 方法。然后,程序尝试编译一段代码,代码中包含了 Point 的 getter() 调用,然后将这段代码添加了另一个类 Line 中,如果代表 Point 的 CtClass 丢失,编译器就无法编译 Line 中的 Point.getter() 方法。注:原来的 Point 类中无 getter() 方法。因此,为了能够正确编译这个方法调用,ClassPool 必须在程序执行期间包含所有的 CtClass 实例。

避免内存溢出

如果 CtClass 对象的数量变得非常大(这种情况很少发生,因为 Javassist 试图以各种方式减少内存消耗),ClassPool 可能会导致巨大的内存消耗。 为了避免此问题,可以从 ClassPool 中显式删除不必要的 CtClass 对象。 如果对 CtClass 对象调用 detach(),那么该 CtClass 对象将被从 ClassPool 中删除。 例如:

CtClass cc = ... ;
cc.writeFile();
cc.detach();

在调用 detach() 之后,就不能调用这个 CtClass 对象的任何方法了。但是如果你调用 ClassPool 的 get() 方法,ClassPool 会再次读取这个类文件,创建一个新的 CtClass 对象。

另一个办法是用新的 ClassPool 替换旧的 ClassPool,并将旧的 ClassPool 丢弃。 如果旧的 ClassPool 被垃圾回收掉,那么包含在 ClassPool 中的 CtClass 对象也会被回收。要创建一个新的 ClassPool,参见以下代码:

ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()

这段代码创建了一个 ClassPool 对象,它的行为与 ClassPool.getDefault() 类似。 请注意,ClassPool.getDefault() 是为了方便而提供的单例工厂方法,它保留了一个ClassPool的单例并重用它。getDefault() 返回的 ClassPool 对象并没有特殊之处。

注意:new ClassPool(true) 构造一个 ClassPool 对象,并附加了系统搜索路径。
调用此构造函数等效于以下代码:

ClassPool cp = new ClassPool();
cp.appendSystemPath();  // or append another path by appendClassPath()

级联的 ClassPools

如果程序正在 Web 应用程序服务器上运行,则可能需要创建多个 ClassPool 实例; 应为每一个 ClassLoader 创建一个 ClassPool 的实例。 程序应该通过 ClassPool 的构造函数,而不是调用 getDefault() 来创建一个 ClassPool 对象。
多个 ClassPool 对象可以像 java.lang.ClassLoader 一样级联。 例如,

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");

如果调用 child.get(),子 ClassPool 首先委托给父 ClassPool。如果父 ClassPool 找不到类文件,那么子 ClassPool 会尝试在 ./classes 目录下查找类文件。

如果 child.childFirstLookup 返回 true,那么子类 ClassPool 会在委托给父 ClassPool 之前尝试查找类文件。 例如:

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath();         // the same class path as the default one.
child.childFirstLookup = true;    // changes the behavior of the child.

拷贝一个已经存在的类来定义一个新的类

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");

这个程序首先获得类 Point 的 CtClass 对象。然后它调用 setName() 将这个 CtClass 对象的名称设置为 Pair。在这个调用之后,这个 CtClass 对象所代表的类的名称 Point 被修改为 Pair。类定义的其他部分不会改变。

注意:CtClass 中的 setName() 改变了 ClassPool 中的记录。从实现的角度来看,一个 ClassPool 对象是一个 CtClass 对象的哈希表。setName() 更改了与哈希表中的 CtClass 对象相关联的 Key。Key 从原始类名更改为新类名。

因此,如果后续在 ClassPool 对象上再次调用 get("Point"),则它不会返回变量 cc 所指的 CtClass 对象。 而是再次读取类文件 Point.class,并为类 Point 构造一个新的 CtClass 对象。 因为与 Point 相关联的 CtClass 对象不再存在。示例:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point");   // cc1 is identical to cc.
cc.setName("Pair");
CtClass cc2 = pool.get("Pair");    // cc2 is identical to cc.
CtClass cc3 = pool.get("Point");   // cc3 is not identical to cc.

cc1 和 cc2 指向 CtClass 的同一个实例,而 cc3 不是。 注意,在执行 cc.setName("Pair") 之后,cc 和 cc1 引用的 CtClass 对象都表示 Pair 类。

ClassPool 对象用于维护类和 CtClass 对象之间的一对一映射关系。 为了保证程序的一致性,Javassist 不允许用两个不同的 CtClass 对象来表示同一个类,除非创建了两个独立的 ClassPool。

如果你有两个 ClassPool 对象,那么你可以从每个 ClassPool 中,获取一个表示相同类文件的不同的 CtClass 对象。 你可以修改这些 CtClass 对象来生成不同版本的类。

通过重命名冻结的类来生成新的类

一旦一个 CtClass 对象被 writeFile() 或 toBytecode() 转换为一个类文件,Javassist 会拒绝对该 CtClass 对象的进一步修改。因此,在表示 Point 类的 CtClass 对象被转换为类文件之后,你不能将 Pair 类定义为 Point 的副本,因为在 Point 上执行 setName() 会被拒绝。 以下代码段是错误的:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair");    // wrong since writeFile() has been called.

为了避免这种限制,你应该在 ClassPool 中调用 getAndRename() 方法。 例如:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");

如果调用 getAndRename(),ClassPool 首先读取 Point.class 来创建一个新的表示 Point 类的 CtClass 对象。 而且,它会在这个 CtClass 被记录到哈希表之前,将 CtClass 对象重命名为 Pair。因此,getAndRename() 可以在表示 Point 类的 CtClass 对象上调用 writeFile() 或 toBytecode() 后执行。

3. 类加载器 (Class Loader)

如果事先知道要修改哪些类,修改类的最简单方法如下:

  1. 调用 ClassPool.get() 获取 CtClass 对象,
  2. 修改 CtClass
  3. 调用 CtClass 对象的 writeFile() 或者 toBytecode() 获得修改过的类文件。

如果在加载时,可以确定是否要修改某个类,用户必须使 Javassist 与类加载器协作,以便在加载时修改字节码。用户可以定义自己的类加载器,也可以使用 Javassist 提供的类加载器。

3.1 CtClass.toClass()

CtClass 的 toClass() 方法请求当前线程的上下文类加载器,加载 CtClass 对象所表示的类。要调用此方法,调用者必须具有相关的权限; 否则,可能会抛出 SecurityException。示例:

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() 。
注意:上面的程序要正常运行,Hello 类在调用 toClass() 之前不能被加载。 如果 JVM 在 toClass() 调用之前加载了原始的 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());

可以工作。你应该给 toClass() 传递加载了你的程序的类加载器(上例中,bean对象的类)。

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

3.2 Java的类加载机制

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

注意:JVM 不允许动态重新加载类。一旦类加载器加载了一个类,它不能在运行时重新加载该类的修改版本。因此,在JVM 加载类之后,你不能更改类的定义。但是,JPDA(Java平台调试器架构)提供有限的重新加载类的能力。参见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 class。 对象 obj 是由 myLoader 加载的 Box 类的一个实例。 因此,最后一个语句总是抛出 ClassCastException ,因为 obj 的类是一个不同的 Box 类的类型,而不是用作变量 b 的类型。

多个类加载器形成一个树型结构。 除引导类加载器之外的每个类加载器,都有一个父类加载器,它通常加载该子类加载器的类。 因为加载类的请求可以沿类加载器的这个层次委派,所以即使你没有请求加载一个类,它也可能被加载。因此,已经请求加载类 C 的类加载器可以不同于实际加载类 C 的加载器。为了区分,我们将前加载器称为 C 的发起者,将后加载器称为 C 的实际加载器 。

此外,如果请求加载类 C(C的发起者)的类加载器 CL 委托给父类加载器 PL,则类加载器 CL 不会加载类 C 引用的任何类。因为 CL 不是那些类的发起者。 相反,父类加载器 PL 成为它们的启动器,并且加载它们。

请参考下面的例子来理解:

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 的实际加载器相同。 因此,Point 不会被 L 加载。

接下来,看一个稍微修改过的例子:

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。 在这种情况下,如果请求加载 Point,类加载器 L 也必须委托给 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 对象的封装。 例如,字段 x 在 PL 中加载的 Point 中是私有的。 然而,如果 L 加载具有以下定义的 Point,则 Window 类可以直接访问 x 的值:

public class Point {
    public int x, y;    // not private
    public int getX() { return x; }
        :
}

有关 Java 类加载器的更多详细信息,可以参看以下文章:

Sheng Liang 和 Gilad Bracha,“Dynamic Class Loading in the Java Virtual Machine”,* ACM OOPSLA'98 *,pp.36-44,1998。

3.3 使用 javassist.Loader

Javassit 提供一个类加载器 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 添加事件监听器。当类加载器加载类时会通知监听器。事件监听器类必须实现以下接口:

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

当事件监听器通过 addTranslator() 添加到 javassist.Loader 对象时,start() 方法会被调用。在 javassist.Loader 加载类之前,会调用 onLoad() 方法。可以在 onLoad() 方法中修改被加载的类的定义。

例如,下面的事件监听器在类加载之前,将所有类更改为 public 类。

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 不能访问 loader 类,如 Main2,MyTranslator 和 ClassPool,因为它们是由不同的加载器加载的。 应用程序类由 javassist.Loader 加载,而加载器类(例如 Main2)由默认的 Java 类加载器加载。

javassist.Loader 以不同的顺序从 java.lang.ClassLoader 中搜索类。ClassLoader 首先将加载操作委托给父类加载器,只有当父类加载器无法找到它们时才尝试自己加载类。另一方面,javassist.Loader 尝试在委托给父类加载器之前加载类。它仅在以下情况下进行委派:

  1. 在 ClassPool 对象上调用 get() 找不到这个类;
  2. 这些类已经通过 delegateLoadingOf() 来指定由父类加载器加载。

此搜索顺序允许 Javassist 加载修改过的类。但是,如果找不到修改的类,它将委托父类加载器来加载。一旦一个类被父类加载器加载,那个类中引用的其他类也将被父类加载器加载,因此它们是没有被修改的。 回想一下,C 类引用的所有类都由 C 的实际加载器加载的。如果你的程序无法加载修改的类,你应该确保所有使用该类的类都是由 javassist 加载的。

3.4 自定义类加载器

下面看一个简单的带 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() 指定。然后运行:

% java SampleLoader

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

这是使用 Javassist 的最简单的方法。 但是,如果你编写一个更复杂的类加载器,你可能需要更详细地了解 Java 的类加载机制。 例如,上面的程序将 MyApp 类放在与 SampleLoader 类不同的命名空间中,因为这两个类由不同的类装载器加载。 因此,MyApp 类不能直接访问类 SampleLoader。

3.5 修改系统的类

像 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

可以使用 MyApp 这样测试修改过的 String 类:

% 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 运行时二进制代码许可协议。

3.6 在运行时重新加载类

如果 JVM 在启用 JPDA(Java平台调试器体系结构)的情况下启动,那么类可以被动态地重新加载。在 JVM 加载类之后,旧版本的类可以被卸载,新版本可以再次重新加载。也就是说,该类的定义可以在运行时动态被修改。然而,新的类定义必须与旧的类定义有些兼容。JVM 不允许两个版本之间的模式更改。它们必须具有相同的方法和字段。

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


下一篇:Javassist 使用指南(二)

你可能感兴趣的:(Javassist 使用指南(一))