对 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 类加载