Javassist
什么是Javassist
Javassist是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶滋)所创建的。
Javassist是一个开源
的分析
、编辑
和创建
Java字节码的类库。它已加入开源的JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。
java字节码的处理,有很多工具,如bcel,asm。不过这些都需要直接跟虚拟机指令打交道。如果你不想了解虚拟机指令,可以采用javassist。javassist是jboss的一个子项目,其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
Java 字节码以二进制的形式存储在 .class 文件中,每一个.class 文件包含一个 Java 类或接口。
Javassist 就是一个用来处理 Java 字节码的类库。它可以在一个已经编译好
的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解。同时也可以去生成一个新的类对象,通过完全手动的方式。
一句话来讲Javassist:Javassist允许Java程序可以在运行时定义一个新的class、在JVM加载时修改class文件。
Javassist的用法
Javassist使用指南按如下目录介绍(后面只介绍一点目前比较重要的点,详细的请看官方文档)
- Reading and writing bytecode
- ClassPool
- Class loader
- Introspection and customization
- Bytecode level API
- Generics
- Varargs
- J2ME
- Boxing/Unboxing
- Debug
Maven依赖:
1 | <dependency> |
1. 读、写字节码
Javassist is a class library for dealing with Java bytecode. Java bytecode is stored in a binary file called a class file. Each class file contains one Java class or interface.
ClassPool对象
:代表class文件的CtClass
对象的容器,可以通过该对象来获取想要读取或者修改的类。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 | //toClass请求当前线程的上下文类加载器去加载class文件,返回一个java.lang.Class对象。 |
示例:
1 | public class WriteRead { |
执行之后会自动生成如下文件cc.writeFile();
可以填一个dirctory Name
的参数,有这个参数,则com.alter.Javassist.test.Rectangle
被包含在这个文件下。
2. Frozen class
冻结类的含义:
如果一个CtClass
对象通过writeFile()
、toBytecode
、toClass
方法被转换到class文件中,javassist则会冻结这个CtClass
对象。再对这个CtClass
对象进行操作则会不允许,这在尝试去修改一个已经被JVM加载过的class文件的时候会发出警告,因为JVM不允许重加载一个class。
一个冻结的CtClass
可以通过其defrost()
方法解冻,解冻后可以允许对这个CtClass
修改:
1 | // 被冻结了,不能再修改(Exception in thread "main" java.lang.RuntimeException: com.alter.Javassist.test.Rectangle class is frozen) |
3. 定义一个新的class
使用ClassPool.makeClass
,重新定义一个新的类
1 | ClassPool pool1 = ClassPool.getDefault(); |
这个程序定义了一个Point
类,未包含任何成员。
成员方法可以通过使用CtClass的addMethod()
方法传入一个CtMethod
的工厂方法创建的对象作为参数来追加。
1 | // 定义一个新的类 |
makeClass()
方法不能创建一个新的接口,需要使用makeInterface()
方法才可以。
接口中的成员方法可以通过CtMethod的abstractMethod
方法创建。
4. 添加成员字段属性
1 | ClassPool pool = ClassPool.getDefault(); //获取一个类池 |
5. 构造函数的创建
1 | // 添加无参构造函数 |
当如果想要实现有参和无参构造函数方法的时候,对于$0
和 $1...$n
,这里的$0
代表着这个类的this
对象,$1
则是当前方法的第一个参数,以此类推!
需要注意:在实现有参和无参构造的时候,一定要setbody
6. 添加成员方法
这里实现成员方法,然后进行保存字节码
1 | // 添加成员方法 如果没有setBody则是一个抽象方法 |
将4
5
6
合起来
执行结果为
7. 修剪prune类
如果CtClass.prune()
方法被调用,则Javassist会在CtClass
被冻结的时候(调用writeFile()
、doBytecode
、toClass
方法的时候)修剪CtClass
对象的数据结构。
为了降低内存消耗,修剪时会放弃对象中的不必要的属性。当一个CtClass
对象被修剪后,方法的字节码则不能被访问,除了方法名称、方法签名和注解。修剪过的CtClass
对象不会被解冻。默认修剪值是false
。
1 | // 修剪ctClass |
禁止修剪stopPruning(true),必须在对象的前面调用:
1 | CtClasss cc = ...; |
注意:
当debugging的时候,你可能想临时禁止修剪、冻结和修改一个class文件到磁盘中,那么debugWriteFile是一个简便的方法。该方法禁止修剪、写入class文件、解冻。
8. 附加
(1) 关于ClassPoolClassPool
需要关注的方法:getDefault
: 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;appendClassPath
, insertClassPath
: 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;toClass
: 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class。get
,getCtClass
: 根据类路径名获取该类的CtClass对象,用于后续的编辑。
(2) 关于CtMethod
CtMethod
继承CtBehavior
,需要关注的方法:insertBefore
在方法的起始位置插入代码insterAfter
在方法的所有 return 语句前插入代码insertAt
在指定的位置插入代码setBody
将方法的内容设置为要写入的代码,当方法被abstract修饰时,该修饰符被移除make
创建一个新的方法
(3) 对象实例化
这里提供了三种方法:
1、反射方式调用
2、加载class文件
3、通过接口
9. AOP编程
想要实现的效果是能够在hello方法的前后进行打印字符==================
insertBefore
在方法的起始位置插入代码insterAfter
在方法的所有 return 语句前插入代码
要知道,class在没有进行toClass
之前,我们可以进行任意修改,我们就拿class字节码文件来继续进行修改:
1 | public class Aop1 { |
这里有个注意点:如果当前web路径中也存在一个com.java.javassist.pojo.People
,那么就需要用到insertClassPath
,当java加载的时候首先找的是class字节码,而不是当前web路径中的类,如果你把insertClassPath
改成了appendClassPath
那么他就会加载失败!
如果只有一个com.java.javassist.pojo.People
,那么使用insertClassPath
还是appendClassPath
都可以(至少这个例子是可以的)。
Reference
http://www.javassist.org/tutorial/tutorial.html
https://www.cnblogs.com/zpchcbd/p/14835338.html
https://blog.csdn.net/zixiao217/article/details/88803631