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 PLprivate int x, y;public int getX() { return x; }:
}public class Box {      // the initiator is L but the real loader is PLprivate Point upperLeft, size;public int getBaseX() { return upperLeft.x; }:
}public class Window {    // loaded by a class loader Lprivate 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 PLprivate Point upperLeft, size;public Point getSize() { return size; }:
}public class Window {    // loaded by a class loader Lprivate 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 privatepublic 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 文档。

2.ClassPool

ClassPool是一个CtClass的容器。因为编译器随时可能访问一个CtClass类,所以一旦一个CtClass创建,它将永远保存在ClassPool类里面。
举一个简单的例子,之前我们有一个叫做表示Point类的CtClass实例,我们在里面添加了一个getter()方法。如果这个操作没有被永远地保存,在另外一处使用这个getter方法时又得重新添加。好在不是如此,ClassPool一直保存着这个实例。

2.1 避免OOM

因为ClassPool上述特性,随着CtClass越来越多,ClassPool的内存开销会越来越大。为了避免这个问题,我们可以移除一些不必要的CtClass类。我们调用CtClass::detach()方法执行这个操作。

CtClass ctClass = pool.makeClass(inputStream);
ctClass.writeFile();
ctClass.detach();

我们调用了detach后,就不能再调用CtClass的任何方法了。在Javassist3.0中,再次调用该命令会抛出RunTimeException.
另外一个方法是我们每次使用的时候手工创建一个ClassPool,旧的ClassPool在没有引用后就会被垃圾回收器回收,我们可以模仿ClassPool的getDefault方法。

ClassPool cp = new ClassPool();
//append ClassPath

的方法。

2.2级联ClassPools

如果这是一个Web程序,那么我们需要多个ClassPool,我们最好为每一个ClassLoader创建一个ClassPool。我们直接调用ClassPool的构造函数而非getDefault方法。

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.

当我们调用child.get()的时候,程序会首先在parent ClassPool中需找结果,如果没有找到,会尝试在child ClassPool中寻找。
如果我们想优先在child ClassPool中寻找,我们可以将child.childFirstLookup设置为true

2.3改变类名以定义新类

通过改变一个已有的类的名字来定义新类,代码如下:

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

这段代码首先从ClassPool中取出一个CtClass,然后调用setName的方法。在这次调用后,类名将更改为Pair,但类的其他内容并未发生改变。(举个例子,如果先前有setX的方法,新类也有)。
需要注意的是,当我们调用setName的时候,将直接在原有类上做修改,不仅如此,先前我们提到ClassPool的实现是以name为key的HashTable,执行完setName之后,hashtable上面的key也会被改变。我们可以通过一段代码验证。

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.

在Javassist中,一个ClassPool里面的name-object键值对都是1对1的关系。除非我们使用两个不同的ClassPool,Javassist不允许两个不同的CtClass拥有同一个类名。这个是Javassist的一个重要特征。
如果我们需要同一个类名有多个不同的版本,我们需要再创建一个ClassPool,然后修改新ClassPool中的CtClass。下面是一个例子。

ClassPool pool = ClassPool.getDefault();
ClassPool qool = new ClassPool();
qool.appendSystemPath();

2.4改变冻结类类名

当一个CtClass执行完writeFile或者toBytecode操作后,Javassist就会拒绝所有对这个CtClass的变更。上上述例子中,如果我们已经把Point冻结,那么我们执行setName将报错。

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

为了避开这个限制,我们可以调用getAndRename的方法,例如

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

当我们调用getAndRename的时候,ClassPool会读取Point.class并创建对应的CtClass的类。然后再执行Rename的操作。

2.5 getAndRename

事实上,如果我们需要一个类的副本,最快捷的方法便是使用getAndRename,我们点进去getAndRename的实现,会发现这个操作的过程是重新从文件中再加载一遍该类,然后再添加进ClassPool。
我们通过一个简单的例子来看一下。

链接:https://www.jianshu.com/p/07da939f0101

Javassist 使用指南 侵立删相关推荐

  1. 消息队列探秘-RabbitMQ消息队列介绍 侵立删

    1. 历史 RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现.AMQP 的出现其实也是应了广大人民群众的需求,虽然在同步消息通讯的世界里有 ...

  2. Java并发编程73道面试题及答案 —— 面试稳了 侵立删

    作者:乌枭 来自:https://blog.csdn.net/qq_34039315/article/details/78549311 最近后台和微信理有很多读者让我整理一些面试题,我就把这事放在心上 ...

  3. 一行代码完成Java的Excel读写 侵立删

    转自:https://mp.weixin.qq.com/s/X9341NNXZe0tkbQu4xbi8Q 前段时间在 github 上发现了阿里的 EasyExcel 项目,觉得挺不错的,就写了一个简 ...

  4. SpringMVC工作原理 侵立删

    转自:http://www.cnblogs.com/xiaoxi/ SpringMVC的工作原理图: SpringMVC原理 1.用户发送请求至前端控制器DispatcherServlet. 2. D ...

  5. JVM知识点精华汇总 侵立删

    转自:https://blog.csdn.net/huyuyang6688/article/details/81490570 本文是学习了<深入理解Java虚拟机>之后的总结,主要内容都来 ...

  6. 先码后看 severlet开发基础 侵立删

    转自:www.cnblogs.com/xdp-gacl/p/3763559.html 一.ServletConfig讲解 1.1.配置Servlet初始化参数 在Servlet的配置文件web.xml ...

  7. HashMap?面试?我是谁?我在哪? 侵立删

    来源:cnblogs.com/zhuoqingsen/p/HashMap.html 现在是晚上11点了,学校屠猪馆的自习室因为太晚要关闭了.勤奋且疲惫的小鲁班也从屠猪馆出来了,正准备回宿舍洗洗睡,由于 ...

  8. Redis 的数据结构和对象系统 侵立删

    Redis是一个开源的 key-value 存储系统,它使用六种底层数据结构构建了包含字符串对象.列表对象.哈希对象.集合对象和有序集合对象的对象系统.今天我们就通过12张图来全面了解一下它的数据结构 ...

  9. 内置对象(转,侵立删)

    1.Request对象 该对象封装了用户提交的信息,通过调用该对象相应的方法可以获取封装的信息,即使用该对象可以获取用户提交的信息. 当Request对象获取客户提交的汉字字符时,会出现乱码问题,必须 ...

最新文章

  1. 2022-2028年中国轻型输送带行业市场发展规模及市场分析预测报告
  2. Centos 76分布式lamp平台
  3. Java里的按值传递与引用传递
  4. 深入理解K-Means聚类算法
  5. Python中NotImplementedError的使用方法(抽象类集成子类实现)
  6. MySQL抛出 Lock wait timeout exceeded; try restarting transaction
  7. AndroidOpenCV摄像头预览全屏问题
  8. j2me模拟器java游戏存档修改,j2me loader模拟器中文
  9. [硬件基础] 电机学基础与常用电工定律
  10. 单片机编程用什么软件?单片机开发软件有哪些?华维告诉你.
  11. 【IE插件】--如何制作?
  12. 《CCNet: Criss-Cross Attention for Semantic Segmentation》--阅读笔记-ICCV2019
  13. performSelector一系列方法调用和延时调用导致的内存泄露
  14. 华为帐号“一号畅玩”体验,助力游戏用户增长
  15. 算法套路学习笔记(第二章) 动态规划系列 2.13-2.19
  16. PDF怎么修改文字,PDF修改文字操作方法
  17. 计算机、网络安全、CTF资源总结-The_Growth_Path_Of_A_Pwner(一名安全从业者的成长之路)
  18. 【jquery事件】
  19. RHCE6.0那点事----仅供参考
  20. 2010年《杨卫华谈微博架构》视频摘抄

热门文章

  1. 在 VMWare Player 中创建 Windows Server 虚拟机
  2. LICEcap(GIF屏幕录制工具)简单使用说明
  3. 什么是SEM,标准误
  4. linux操作系统版本 3100,IBM SYSTEM x3100 都能安装哪些操作系统?
  5. app逆向篇之实战案例-某音乐app
  6. HCIE之路-13 华为MPLS基础思维导图(不定期更新,纯个人理解,欢迎批评指正!!!)
  7. 安徽省计算机二级科目有哪些内容,计算机二级考试有哪些科目可以选择?
  8. 学习如何制作游戏宣传视频
  9. STC32G12K128-Beta 硬件USB直接ISP下载
  10. 一步一步CocosBuilder(2)