对 Java Javassist 使用的简单介绍

Javassist 简介
Javassist(Java 编程助手)使 Java 字节码操作变得简单。它是一个用于在 Java 中编辑字节码的类库。它使 Java 程序可以在运行时定义新类,并在 JVM 加载它时修改类文件。与其他类似的字节码编辑器不同,Javassist 提供了两个级别的 API:源级别和字节代码级别。如果用户使用源代码级 API,则他们可以在不了解 Java 字节码规范的情况下编辑类文件。整个 API 仅使用 Java 语言的词汇表进行设计。甚至可以以源文本的形式指定插入的字节码。Javassist 可以即时对其进行编译。另一方面,字节码级别的 API 允许用户像其他编辑器一样直接编辑类文件
Javassist 是用于编辑(创建,修改).class字节码文件的 Java 库,一般情况下我们都是在.java文件中写代码,然后编译成.class文件,在加载进 Java 虚拟机中执行代码,如果要修改已编译好的文件,要使用010 Editor去手动的计算一些偏移值进行修改,但是 Javassist 的出现,使得我们操作.class文件变得简单,并且可以在 Java 虚拟机 JVM 运行时动态地改变.class文件
添加依赖:
1 | <dependencies> |
读取写入字节码
读写字节码
javassist.CtClass代表一个 class 文件的抽象类表示形式,一个CtClass(compile-time class 编译时的类)是一个处理 class 文件的句柄,以下是一个简单的程序:
1 | ClassPool pool = ClassPool.getDefault(); |
这段程序首先包含一个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():
1 | byte[] b = cc.toBytecode(); |
也可以像这样直接加载CtClass:
1 | Class clazz = cc.toClass(); |
toClass请求当前线程的上下文类加载器去加载 class 文件,返回一个java.lang.Class对象,实例:
1 | import javassist.CannotCompileException; |
使用的两个类Rectangle和Point不需要属性和方法,是两个空类
定义新的类
重新定义一个新的类,需要调用ClassPool.makeClass:
1 | import javassist.ClassPool; |
这个程序定义了一个Point类,未包含任何成员,同样只作用于 JVM,成员方法可以通过使用CtClass的addMethod()方法传入一个CtMethod的工厂方法创建的对象作为参数来追加:
1 | import javassist.ClassPool; |
makeClass()方法不能创建一个新的接口,需要使用makeInterface()方法才可以。 接口中的成员方法可以通过CtMethod的abstractMethod方法创建,还可以添加已存在的方法到新类:
1 | import javassist.ClassPool; |
自定义一个类加载器来加载我们的类
冻结类
如果一个CtClass对象通过writeFile()、doBytecode、toClass方法被转换到 class 文件中,javassist 则会冻结这个CtClass对象。再对这个CtClass对象进行操作则会不允许,这在开发者他们尝试去修改一个已经被 JVM 加载过的 class 文件的时候会发出警告,因为 JVM 不允许重加载一个 class:
1 | import javassist.ClassPool; |
修剪类
如果CtClass.prune()方法被调用,则 javassist 会在 CtClass 被冻结的时候(调用writeFile()、doBytecode、toClass方法的时候)会修剪 CtClass 对象的数据结构。 为了降低内存消耗,修剪时会放弃对象中的不必要的属性。当一个 CtClass 对象被修剪后,方法的字节码则不能被访问除了方法名称、方法签名和注解。修剪过的 CtClass 对象不会被解冻。默认修剪值是 false:
1 | import javassist.ClassPool; |
禁止修剪stopPruning(true),必须在对象的前面调用:
1 | CtClasss cc = ...; |
类路径
默认的ClassPool.getDefault()检索路径和 JVM 底层路径一致(classpath),添加类路径:
1 | // 添加 class 查找路径 |
更多内容参考:读、写字节码
ClassPool
一个ClassPool对象是包含CtClass对象的容器。一旦一个CtClass对象被创建后,就会被记录到一个ClassPool中。这是因为编译器在编译源码时会引用代表CtClass的类,可能会访问CtClass对象
比如,假设一个新的方法getter()被添加到一个代表Point类的CtClass对象中。之后,程序尝试编译Point中包含调用getter()方法的源代码,并且使用编译后的代码作为方法的方法体,将其添加到另一个类Line中。如果代表Point的CtClass对象丢失了,编译器则不能编译Line中调用getter()的方法。注意:原来定义的类是不包含getter()方法的。因此,为了正确编译这样的方法调用,ClassPool必须包含程序执行时所有的CtClass实例
移除 CtClass
可以调用CtClass的detach()方法,然后会将该对象从ClassPool中移除:
1 | ClassPool classPool = ClassPool.getDefault(); |
级联 ClassPool
多个 ClassPool 对象可以像java.lang.ClassLoader一样级联:
1 | // 级联 ClassPool |
如果调用了child.get()方法,child 首先委托父 ClassPool,如果父 ClassPool 加载 class 文件失败,然后 child 再尝试从./classes/目录下查找类文件,如果child.childFirstLookup设置为 true,则 child 尝试在委托给父 classPool 之前去加载 class 文件
更改类名
一个新的 class 可以被定义为一个已存在的类的副本:
1 | // 首先获取表示 Point 类的一个 CtClass 对象 |
这个程序首先包含类 Point 的 ctClass 对象,然后调用setName()方法为 CtClass 对象设置新的名称。在这个调用之后,所有由该 CtClass 对象定义的所有类名展示均由 Point 变更为 Pair,类定义的其他部分则不会变更
为了创建一个默认 ClassPool 实例(Clas.getDefault()返回的)的一个副本,可以使用以下代码片段:
1 | ClassPool pool10 = ClassPool.getDefault(); |
更多内容参考:ClassPool 类池
Class Loader
toClass 方法
CtClass提供了一个便捷的方法toClass,请求当前线程的类加载器去加载 CtClass 表示的类。调用此方法必须具有适应的权限,否则会抛出一个SecurityException异常:
1 | import javaclass.Javassist3ClassLoader; |
首先,通过测试类的表示Javassist3ClassLoader类的CtClass修改其say方法,在方法体前面增加一行输出语句; 然后,通过CtClass的 toClass 方法请求当前线程(Javassist3ClassLoaderTest 类所在的线程)去加载 Javassist3ClassLoader 类; 最后,通过 Class 对象的静态方法 newInstance 构造一个 Javassist3ClassLoader 对象,并调用其 say 方法,得到字节码修改后的方法执行内容结果
注意: 上面的程序依赖于 Javassist3ClassLoaderTest 类所在的类加载器在调用toClass之前没有加载过 Javassist3ClassLoader 类
如果程序运行在 Web 容器中例如 JBoss、Tomcat 中, 上下文的类加载器使用
toClass()方法可能并不适当。在这种情况下,你可能会看到一个不期望的异常ClassCastException。为了避免这种情况,你必须明白清楚地给定一个适当的类加载器给toClass方法。例如,如果bean是你的会话的 bean 对象:
1
2 CtClass cc = ...
Class c = cc.toClass(bean.getClass().getClassLoader());
Javassist Loader
javassist 提供了一个类加载器javassist.Loader,这个类加载器使用javassist.ClassPool对象读取 class 文件,例如,javassist.Loader可用于使用 javassist 修改的指定的类:
1 | ClassPool pool = ClassPool.getDefault(); |
这个程序修改了类 Rectangle 类,将其父类设置为 Point 类,然后程序加载了修改后的 Rectangle 类,并且创建了一个实例
更多内容参考:Class loader 类加载