Javassist是一个处理字节码的类库。Java字节码存储在一个叫做*.class的二进制文件中。每个class文件包含一个java类或者接口。
javassist.CtClass
代表一个class文件的抽象类表示形式。一个CtClass
(compile-time class编译时的类)是一个处理class文件的句柄,以下是一个简单的程序:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
这段程序首先包含一个ClassPool
对象,通过javassist控制字节码的修改。ClassPool
对象是代表class文件的CtClass
对象的容器。它根据构造一个CtClass
对象的需求读取一个class文件,并记录被构建好的对象以供将来进行访问。
为了修改一个类的定义,用户必须首先从ClassPool
对象的.get(className)
方法获取一个CtClass
引用。
在上述示例中,CtClass
对象表示ClassPool
中的类test.Rectangle
,并且将其分配给变量cc
。
ClassPool
对象由静态方法getDefault
方法查找默认的系统检索path返回。
从实现上来看,
ClassPool
是一个CtClass
的哈希表,使用class name作为key。
ClassPool.get()
方法通过检索这个哈希表找到一个CtClass
对象关联指定的key。如果
CtClass
对象没有找到,get()
方法会读取class文件去构造一个CtClass
对象,记录在哈希表中然后作为get()
的返回值返回。
从ClassPool
中获取到的CtClass
对象是可以被修改的。在上述示例中,它被修改了, test.Rectangle
的父类变更为test.Point
,这个修改将会在最后CtClass.writeFile()
方法调用后反映在class文件中。
writeFile()
方法将CtClass
对象转换到class文件并且将其写入本地磁盘。Javassist也提供了一个方法用于直接获取修改后的字节码:toBytecode()
:
byte[] b = cc.toBytecode();
也可以像这样直接加载CtClass
:
Class clazz = cc.toClass();
toClass
请求当前线程的上下文类加载器去加载class文件,返回一个java.lang.Class
对象。
示例:
package org.byron4j.cookbook.javaagent;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import java.io.IOException;
public class Hello {
public void sayHello(){
System.out.println("Hello!");
}
public static void main(String[] args){
// 获取ClassPool
ClassPool pool = ClassPool.getDefault();
CtClass cc = null;
try {
// 通过ClassPool获取CtClass
cc = pool.get("org.byron4j.cookbook.javaagent.Rectangle");
// 设置父类
cc.setSuperclass(pool.get("org.byron4j.cookbook.javaagent.Point"));
// 更新到class文件中(仅在JVM中)
cc.writeFile();
// 获取修改后的字节码
byte[] b = cc.toBytecode();
System.out.println(new String(b));
// 加载类(请求当前线程的上下文加载器加载CtClass代表的类)
Class clazz = cc.toClass();
System.out.println("superClass is :" + clazz.getSuperclass());
} catch (NotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
}
}
}
重新定义一个新的类,ClassPool.makeClass
方法将会被调用:
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("hello.make.Point");
System.out.println(cc2.toClass()); // 输出class hello.make.Point
这个程序定义了一个Point
类,未包含任何成员,成员方法可以通过使用CtClass
的addMethod()
方法传入一个CtMethod
的工厂方法创建的对象作为参数来追加。
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("hello.make.Point");
//System.out.println(cc2.toClass().getMethods().length); // 9
// 追加方法
cc2.addMethod(CtMethod.make("public void sayHello(){\n" +
" System.out.println(\"Hello!\");\n" +
" }",cc2));
System.out.println(cc2.toClass().getMethods().length); // 10
makeClass()
方法不能创建一个新的接口,需要使用makeInterface()
方法才可以。
接口中的成员方法可以通过CtMethod
的abstractMethod
方法创建。
冻结类的含义
如果一个
CtClass
对象通过writeFile()
、doBytecode
、toClass
方法被转换到class文件中,javassist则会冻结这个CtClass
对象。再对这个CtClass
对象进行操作则会不允许,这在开发者他们尝试去修改一个已经被JVM加载过的class文件的时候会发出警告,因为JVM不允许重加载一个class。
一个冻结的CtClass
可以通过其defrost()
方法解冻,解冻后可以允许对这个CtClass修改:
// 被冻结了,不能再修改(Exception in thread "main" java.lang.RuntimeException: hello.make.Point class is frozen)
// 解冻后可以修改
cc2.toBytecode();// 被冻结
cc2.defrost();// 解冻
System.out.println(cc2.getFields().length);
cc2.addField(CtField.make("private String name;", cc2));// 解冻后允许修改
cc2.writeFile();
System.out.println(cc2.getFields().length);
如果CtClass.prune()
方法被调用,则Javassist会在CtClass被冻结的时候(调用writeFile()
、doBytecode
、toClass
方法的时候)会修剪CtClass对象的数据结构。
为了降低内存消耗,修剪时会放弃对象中的不必要的属性。当一个CtClass对象被修剪后,方法的字节码则不能被访问除了方法名称、方法签名和注解。修剪过的CtClass对象不会被解冻。默认修剪值是false。
// 修剪ctClass
cc2.prune();// 设置修剪伪true
cc2.writeFile();// 冻结的时候,会进行修剪
System.out.println(cc2);//修剪后不能访问方法
禁止修剪stopPruning(true)
,必须在对象的前面调用:
CtClasss cc = ...;
cc.stopPruning(true);// 前面调用禁止修剪
:
cc.writeFile();
注意:
当debugging的时候,你可能想临时禁止修剪、冻结和修改一个class文件到磁盘中,那么
debugWriteFile
是一个简便的方法。该方法禁止修剪、写入class文件、解冻、禁止重新开启修剪(如果开始是打开的话)。
默认的ClassPool.getDefault()
检索路径和JVM底层路径一致(classpath)。如果一个程序运行在一个web应用程序比如JBoss、Tomcat中,ClassPool
对象则可能搜索步到用户的类,因为web应用使用了多个类加载器。在这种情况下,一个额外的classpath必须注册到ClassPool
中。假设pool引用了一个ClassPool对象:
//添加class查找路径
pool1.insertClassPath(new ClassClassPath(this.getClass()));
这条语句注册了this
引用对象加载的类的class path。你可以使用任意的Class
对象作为参数。Class
对象已经被注册上了的表现就是它所在的class path被加载了。
你也可以注册一个目录的名称作为一个class查找路径。例如,以下代码添加了/usr/local/javalib
到class查找路径中:
// 添加文件目录作为calss查找路径
pool1.insertClassPath("/usr/local/javalib");
你还可以添加URL作为class查找路径:
// 添加URL作为class查找路径,第三个参数必须/开头、第四个参数必须.结尾
// 添加 "http://www.javassist.org:80/java/"
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool1.insertClassPath(cp);
这个程序添加了http://www.javassist.org:80/java/
到class查找路径中。这个URL仅仅用来查找org.javassist.
包的类。例如:加载一个org.javassist.test.Main
类,它的class文件是:
http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你还可以直接给一个byte数组去构建一个CtClass
对象,可以使用ByteArrayClassPath
:
// byte数组形式class path
ClassPool pool2 = ClassPool.getDefault();
byte[] arr = "org.byron4j".getBytes();
String name = "org.byron4j.Hello";
pool2.insertClassPath(new ByteArrayClassPath(name, arr));
CtClass ctClass = pool2.get(name);
CtClass
对象ctClass表示字节数组b指定的class文件定义的类实例。ClassPool
从给定的ByteArrayClassPath
读取一个class文件.
如果你不确定类的完全限定名,你可以使用ClassPool
的makeClass
方法:
// makeClass
ClassPool pool3 = ClassPool.getDefault();
InputStream ins = new FileInputStream("/usr/local/javalib");
CtClass ctClass1 = pool3.makeClass(ins);
makeClass
返回一个从给定输入流中构造的CtClass
对象。你可以使用
makeClass
急切地将一个类文件给ClassPool
对象,可以提升性能如果class查找路径包含大量的jar文件的话。因为ClassPool
对象根据需要读取一个class文件,它可能重复为了每个class文件检索全部的jar文件,makeClass()
则可用于优化该搜索方式。通过makeClass
构造出来的CtClass
对象会在ClassPool
中保持存在,且不会再次去读取class文件了。
用户可以扩展class查找路径。可以定义一个新的类实现ClassPath
接口,这种方式可以允许将非标准资源包含到class查找路径中。