Skip to content

读和写字节码

Javassist是一个处理Java字节码的类库。CtClass是类文件的抽象表示。CtClass(编译时类)对象是处理类文件的句柄。下面的程序是一个非常简单的例子:

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

该程序首先获得一个ClassPool对象,用于Javassist控制字节码修改。ClassPool对象是CtClass对象的容器。它通过读取类文件来构造CtClass对象,并记录构造的对象以方便后续的访问。要修改类的定义,用户必须首先从ClassPool对象中获得该类的CtClass对象的引用。即通过ClassPool中的get()。

在上面所示的程序中,CtClass对象表示一个类的抽象定义。通过从ClassPool容器中获取一个矩形类,它被分配给一个变量cc。 使用引用的cc进而设置修改该类的定义,在上面的代码中是分配了一个父类,而父类也是通过ClassPool来获取的,最后在将修改的类抽象写入文件。

从实现的角度来讲,ClassPool是一个哈希表,使用类名作为key,通过get(key)去获取类文件的抽象CtClass,即使get(key)方法没有找到指定的CtClass,也会通过构造方法new一个全新的CtClass对象,记录到哈希表,并返回。

writeFile()将CtClass对象转换为类文件并将其写入本地磁盘。而除此外,Javassist也提供了直接获取修改后的字节码的方法【调用toBytecode()】:

java
byte[] b = cc.toBytecode();

当然也可以直接加载CtClass,获取对应类

java
Class clazz = cc.toClass();

toClass()请求当前线程的上下文类加载器来加载由CtClass表示的类文件。它返回一个java.lang.Class对象,表示加载的类。

定义一个新类

要从头定义一个新类,必须在ClassPool上调用makeClass()。

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

上面的代码定义了一个point类,该类不含任何成员和方法。如果需要添加成员方法,可以通过工厂方法CtNewMethod声明然后通过CtClass对象的addMethod()加入到Point类中。

makeClass()不能创建新接口;在ClassPool中makeInterface()可以做到。接口中的成员方法可以用CtNewMethod中的abstractMethod()创建。

注意,接口方法是一个抽象方法。

锁定类

如果一个CtClass对象被writeFile()、toClass()或toBytecode()转换成一个类文件,Javassist会锁定这个CtClass对象。不允许进一步修改该CtClass对象。这是为了在开发人员试图修改已经加载的类文件时发出警告,因为JVM不允许重新加载类。

锁定的CtClass可以解锁,以便允许修改类定义。例如,

java
CtClasss cc = ...;
    :
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // OK since the class is not frozen.

调用defrost()之后,可以再次修改CtClass对象。

如果ClassPool.doPruning设置为true,那么当Javassist锁定CtClass对象时,Javassist会修剪该对象中包含的数据结构。即为了减少内存消耗,修剪会丢弃该对象中不必要的属性(attribute_info结构)。例如,Code_attribute结构(方法体)被丢弃。因此,在CtClass对象被修剪之后,除了方法名、签名和注释之外,方法的字节码是不可访问的。修改后的CtClass对象不能再解锁。ClassPool的默认值ClassPool.doPruning是false。

要禁止修剪特定的CtClass,必须提前在该对象上调用stopPruning():

java
CtClasss cc = ...;
cc.stopPruning(true);
    :
cc.writeFile();                             // convert to a class file.
// cc is not pruned.

CtClass对象cc没有被修剪。因此,它可以在writeFile()被调用后解锁。

注意:在调试时,如果希望暂时停止修剪和冻结,并将修改后的类文件写入磁盘驱动器。可以使用debugWriteFile()。它会停止剪枝,写入一个类文件,并解锁它,然后再次打开剪枝。

类搜索路径

ClassPool静态方法返回的默认值ClassPool.getDefault() 搜索与底层 JVM(Java 虚拟机)相同的路径。 如果程序运行在诸如JBoss和Tomcat之类的Web应用程序服务器上,则该ClassPool对象可能无法找到用户类 ,因为此类Web应用程序服务器使用多个类加载器以及系统类加载器。在这种情况下,必须将额外的类路径注册到ClassPool. 假设 thatpool 引用一个ClassPool对象:

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

该语句注册用于加载所this引用对象的类的类路径。您可以使用任何Class对象作为参数而不是 this.getClass(). 用于加载该对象表示的类的类路径Class已注册。

您可以将目录名称注册为类搜索路径。例如,以下代码将目录添加 /usr/local/javalib 到搜索路径:

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

用户可以添加的搜索路径不仅是目录,还可以是URL:

java
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

此外,您可以直接给一个字节数组给一个ClassPool对象,并从该数组构造一个CtClass对象。要做到这一点,使用ByteArrayClassPath。例如,

java
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指定的类文件定义的类。如果调用get()并且给get()的类名等于由name指定的类名,则ClassPool从给定的ByteArrayClassPath读取类文件。

如果你不知道类的完全限定名,那么你可以在ClassPool中使用makeClass():

java
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中插入ClassPath()。这允许在搜索路径中包含非标准资源。

Released under the MIT License.