javassist使用指南
目录
- 一、快速入门
- 1.1 创建class文件
- 1.2 ClassPool的相关方法
- 1.3 CtClass的相关方法
- 1.4 CtMethod的相关方法
- 1.5 调用生成的类对象
- 1.5.1 通过反射调用
- 1.5.2 通过接口调用
- 1.6 修改现有的类对象
- 二、将类冻结
- 三、类搜索路径
- 四、$开头的特殊字符
- 五、ProxyFactory的使用
我们知道Java字节码以二进制的形式存储在class文件中,每一个class文件包含一个Java类或接口。Javaassist 就是一个用来处理Java字节码的类库。在Javassist 中,类CtClass表示class文件。
我们可以用javassist类库实现动态创建类、添加类的属性和方法、设置类的父类,以及修改类的方法等操作。Javassist不允许删除方法或字段,但它允许更改名称。所以,如果一个方法是没有必要的,可以通过调用CtMethod的setName和setModifiers中将其改为一个私有方法。Javassist不允许向现有方法添加额外的参数。你可以通过新建一个方法达到同样的效果。
使用前需要先引入javassist库
以gradle工程为例,添加如下依赖:
implementation 'org.javassist:javassist:3.28.0-GA'
一、快速入门
1.1 创建class文件
假设我们需要在当前工程的/out/class目录内创建一个Person.class文件,例如:
idea反编译查看的源码内容如下:
package com.test;public class Person {private String name = "小明";private int age = 20;public void setName(String var1) {this.name = var1;}public String getName() {return this.name;}public void setAge(int var1) {this.age = var1;}public int getAge() {return this.age;}public Person() {this.name = "老王";this.age = 30;}public Person(String var1, int var2) {this.name = var1;this.age = var2;}public void printName() {System.out.println(this.name);}public void printAge() {System.out.println(this.age);}
}
使用javassist可以很方便的实现:
package com.demoimport javassist.*object CreatePersonTest {@JvmStaticfun main(args: Array<String>) {createPerson()}fun createPerson() {// 1.创建默认的ClassPool,ClassPool是一个存储CtClass的Hash表val pool: ClassPool = ClassPool.getDefault()// 2.新建一个空类,叫Person类val person: CtClass = pool.makeClass("com.test.Person")// 3.新增一个字段 private String name;// 字段名为nameval nameField: CtField = CtField(pool.get("java.lang.String"), "name", person)// 设置字段的访问类型为privatenameField.modifiers = Modifier.PRIVATE// 新增age字段,也可以使用make方法快速创建val ageField = CtField.make("private int age;", person)// 给字段进行默认初始化,并添加到Person类中person.addField(nameField, CtField.Initializer.constant("小明"))person.addField(ageField, CtField.Initializer.constant(20))// 3.生成getter、setter方法person.addMethod(CtNewMethod.setter("setName", nameField))person.addMethod(CtNewMethod.getter("getName", nameField))person.addMethod(CtNewMethod.setter("setAge",ageField))person.addMethod(CtNewMethod.getter("getAge",ageField))// 4.添加无参数的构造方法val cons: CtConstructor = CtConstructor(arrayOf<CtClass>(), person)// 设置构造方法的方法体cons.setBody("{name=\"老王\";age=30;}")// 添加到person类中person.addConstructor(cons)// 5.添加有参的构造函数val cons2: CtConstructor = CtConstructor(arrayOf(pool.get("java.lang.String"), CtClass.intType), person)// $0=this, $1,$2,$3...代表第几个参数cons2.setBody("{$0.name=$1;$0.age=$2;}")// 添加到person类中person.addConstructor(cons2)// 6.创建一个名为printName的方法,无参数,无返回值,输出name值val printName: CtMethod = CtMethod(CtClass.voidType, "printName", arrayOf<CtClass>(), person)// 设置方法访问类型printName.modifiers = Modifier.PUBLICprintName.setBody("{System.out.println(name);}")// 上面的方式可以换成这种快速创建val printAge: CtMethod = CtMethod.make("public void printAge(){System.out.println(age);}", person)// 添加到person类中person.addMethod(printName)person.addMethod(printAge)// 7.将创建的类对象编译成.class文件,输出到指定到路径为当前路径下的out/class路径person.writeFile("./out/class/")}
}
跟咱们预想的一样。在 Javassist 中,类 Javaassit.CtClass 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,ClassPool是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。
需要注意的是 ClassPool 会在内存中维护所有被它创建过的 CtClass,当 CtClass 数量过多时,会占用大量的内存,API中给出的解决方案是 有意识的调用CtClass的detach()方法以释放内存。
1.2 ClassPool的相关方法
getDefault
: 返回默认的ClassPool 是单例模式的,一般通过该方法创建我们的ClassPool;
appendClassPath, insertClassPath
: 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置。通常通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类的尴尬;
toClass
: 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的class;
get , getCtClass
: 根据类路径名获取该类的CtClass对象,用于后续的编辑。
1.3 CtClass的相关方法
freeze
: 冻结一个类,使其不可修改;
isFrozen
: 判断一个类是否已被冻结;
prune
: 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
defrost
: 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach
: 将该class从ClassPool中删除;
writeFile
: 根据CtClass生成 .class 文件;
toClass
: 通过类加载器加载该CtClass。
setInterfaces
: 添加父接口
setSuperclass
: 添加父类
1.4 CtMethod的相关方法
上面我们创建一个新的方法使用了CtMethod类。CtMthod代表类中的某个方法,可以通过CtClass提供的API获取或者CtNewMethod新建,通过CtMethod对象可以实现对方法的修改。
insertBefore
: 在方法的起始位置插入代码;
insterAfter
: 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
insertAt
: 在指定的位置插入代码;
setBody
: 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
make
: 创建一个新的方法。
注意到在上面代码中的:setBody()的时候我们使用了一些符号:
cons2.setBody("{$0.name=$1;$0.age=$2;}")
具体还有很多的符号可以使用,但是不同符号在不同的场景下会有不同的含义,后面会介绍,也可以看javassist 的说明文档。
1.5 调用生成的类对象
上面的案例是创建一个类对象然后输出该对象编译完之后的 .class 文件。那如果我们想调用生成的类对象中的属性或者方法应该怎么去做呢?
1.5.1 通过反射调用
通过CtClass的toClass方法可以转化成Class对象,然后就可以通过反射的操作来操作目标类的成员了。
fun callByReflect() {// 1.创建classPoolval pool: ClassPool = ClassPool.getDefault()// 2.添加目标类的搜索路径pool.insertClassPath("./out/class/")// 3.获取Person.class的CtClass对象val person: CtClass = pool.get("com.test.Person")// 4.创建Person对象// 先通过CtClass的toClass让类加载器加载该CtClass,然后使用反射创建对象val personObj = person.toClass().newInstance()// 5.反射调用setter方法val setName = personObj.javaClass.getMethod("setName", String::class.java)setName.invoke(personObj, "老王")// 6.反射执行printName方法val printName = personObj.javaClass.getMethod("printName")printName.invoke(personObj)
}
1.5.2 通过接口调用
上面两种其实都是通过反射的方式去调用,问题在于我们的工程中其实并没有这个类对象,所以反射的方式比较麻烦,并且开销也很大。那么如果你的类对象可以抽象为一些方法得合集,就可以考虑为该类生成一个接口类。这样在newInstance()的时候我们就可以强转为接口,可以将反射的那一套省略掉了。
还拿上面的Person类来说,新建一个IPerson接口类:
package com.demo/*** @author chenyousheng* @date 2022/2/12* @desc Person类接口*/
interface IPerson {fun setName(name: String)fun getName(): Stringfun printName()
}
通过CtClass的setInterfaces方法可以给目标类添加一个父接口,也就是上面我们定义的接口类。
fun callByInterface() {// 1.创建classPoolval pool: ClassPool = ClassPool.getDefault()// 2.添加类搜索路径,这里是IPerson接口class文件所在的路径,// 如果IPerson源码文件的根文件是src目录,那么也可以不需要指定class文件的目录,因为ClassPool默认搜索路径能够找到// pool.appendClassPath("./build/classes/kotlin/main/com/demo/")// 这里是Person类的class所在的路径,由于这个路径不是默认搜索路径,所以需要指定pool.appendClassPath("./out/class")// 3.获取接口类val personInter: CtClass = pool.get("com.demo.IPerson")// 4.获取Person类val person: CtClass = pool.get("com.test.Person")// 5.让Person类实现IPerson接口person.interfaces = arrayOf<CtClass>(personInter)// 6.下面创建Person对象,就可以强转成接口类了val personObj: IPerson = person.toClass().newInstance() as IPerson// 然后就可以愉快的调用方法了,不需要反射来操作了println(personObj.getName())personObj.setName("老王2")personObj.printName()}
1.6 修改现有的类对象
前面说到新增一个类对象。这个使用场景目前还没有遇到过,一般会遇到的使用场景应该是修改已有的类。比如常见的日志切面,权限切面。我们利用javassist来实现这个功能。
有如下类对象:
package com.demo;public class PersonService {public void fly() {System.out.println("我飞起来了");}
}
下面的例子是演示如何来操作PersonService.class文件
package com.demoimport javassist.ClassPool
import javassist.CtClass
import javassist.CtMethod
import javassist.Modifier/*** @author chenyousheng* @date 2022/2/12* @desc 修改已存在的class*/
object UpdatePersonTest {@JvmStaticfun main(args: Array<String>) {update()}fun update() {// 1.创建classPool,使用的是默认系统的类搜索路径。val pool: ClassPool = ClassPool.getDefault()// 2.获取要修改的classval cc: CtClass = pool.get("com.demo.PersonService")// 3.获取目标类的方法val fly: CtMethod = cc.getDeclaredMethod("fly")// 在方法执行前插入代码fly.insertBefore("System.out.println(\"起飞前准备降落伞\");")// 在方法执行后插入代码fly.insertAfter("System.out.println(\"成功落地\");")// 4.新增一个方法val newMethod: CtMethod = CtMethod(CtClass.voidType, "joinFriend",arrayOf<CtClass>(), cc)// 设置方法访问权限newMethod.modifiers = Modifier.PUBLIC// 设置方法的方法体newMethod.setBody("{System.out.println(\"加个好友吧\");}")// 加到目标类中cc.addMethod(newMethod)// 5.实例化目标类val personService = cc.toClass().newInstance()// 调用fly方法personService.javaClass.getMethod("fly").invoke(personService)// 调用joinFriend方法personService.javaClass.getMethod("joinFriend").invoke(personService)}
}
运行结果如下:
起飞前准备降落伞
我飞起来了
成功落地
加个好友吧
另外需要注意的是:上面的insertBefore() 和 setBody()中的语句,如果你是单行语句可以直接用双引号,但是有多行语句的情况下,你需要将多行语句用{}括起来。javassist只接受单个语句或用大括号括起来的语句块。
二、将类冻结
如果一个 CtClass 对象通过 writeFile(), toClass(), toBytecode() 被转换成一个类文件,此 CtClass 对象会被冻结起来,不允许再修改。因为一个类只能被 JVM 加载一次。
但是,一个冷冻的 CtClass 也可以被解冻,例如:
CtClasss cc = ...;:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // 因为类已经被解冻,所以这里可以调用成功
调用 defrost() 之后,此 CtClass 对象又可以被修改了。
如果 ClassPool.doPruning 被设置为 true,Javassist 在冻结 CtClass 时,会修剪 CtClass 的数据结构。为了减少内存的消耗,修剪操作会丢弃 CtClass 对象中不必要的属性。例如,Code_attribute 结构会被丢弃。一个 CtClass 对象被修改之后,方法的字节码是不可访问的,但是方法名称、方法签名、注解信息可以被访问。修剪过的 CtClass 对象不能再次被解冻。ClassPool.doPruning 的默认值为 false。
stopPruning() 可以用来驳回修剪操作。
CtClasss cc = ...;
cc.stopPruning(true);:
cc.writeFile(); // 转换成一个 class 文件
// cc is not pruned.
这个 CtClass 没有被修剪,所以在 writeFile() 之后,可以被解冻。
三、类搜索路径
通过ClassPool.getDefault()获取的ClassPool使用 JVM 的类搜索路径。如果程序运行在JBoss或者Tomcat等 Web 服务器上,ClassPool可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。
1)通过ClassClassPath添加搜索路径
pool.insertClassPath(new ClassClassPath(Person.getClass()));
上面的语句将Person类添加到pool的类加载路径中。但在实践中,我发现通过这个可以将Person类所在的整个jar包添加到类加载路径中。
2)通过指定目录来添加搜索路径
也可以注册一个目录作为类搜索路径:
如pool.insertClassPath("/usr/javalib");
则是将 /usr/javalib目录添加到类搜索路径中。
3)通过URL指定搜索路径
ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.sample.com", 80, "/out/", "com.test.");
pool.insertClassPath(cp);
上述代码将http://www.sample.com:80/out添加到类搜索路径。并且这个URL只能搜索 com.test包里面的类。例如,为了加载 com.test.Person,它的类文件会从获取http://www.sample.com:80/out/com/test/Person.class
获取。
4)通过ByteArrayPath添加搜索路径
ClassPool cp = ClassPool.getDefault();
byte[] buf = 字节数组;
String name = 类名;
cp.insertClassPath(new ByteArrayClassPath(name, buf));
CtClass cc = cp.get(name);
示例中的 CtClass 对象是字节数据buf代表的class文件。将对应的类名传递给ClassPool的get()方法,就可以从字节数组中读取到对应的类文件。
5)通过输入流加载class
如果你不知道类的全名,可以使用makeClass()方法:
ClassPool cp = ClassPool.getDefault();
InputStream ins = class文件对应的输入流;
CtClass cc = cp.makeClass(ins);
makeClass()返回从给定输入流构造的CtClass对象。你可以使用makeClass()将类文件提供给ClassPool对象。如果搜索路径包含大的jar文件,这可能会提高性能。由于ClassPool对象按需读取类文件,它可能会重复搜索整个jar文件中的每个类文件。makeClass()可以用于优化此搜索。由makeClass()构造的CtClass保存在ClassPool对象中,从而使得类文件不会再被读取。
四、$开头的特殊字符
符号 | 含义 |
---|---|
$0, $1, $2, … | $0=this,$1表示方法的第一个参数,依次类推,如果方法是静态的,则 $0 不可用 |
$args | 方法参数数组.它的类型为 Object[],$args[0]=1,1,1,args[1]=$2 |
$r | 返回结果的类型,用于强制类型转换 |
$w | 包装器类型,用于强制类型转换,当放回值是包装类型时,可以用此来强转 |
$_ | 返回值,一般在insertAfter中用到,用于得到原方法的返回值 |
$sig | 参数类型数组,$sig[0]表示第一个参数类型 |
$type | 返回值类型,一般在insertAfter中用到,即$_的类型 |
$class | $0或this的类型 |
$e | 异常类型 |
1)$e的使用场景
在给方法添加catch语句块的时候就会用到了,例如
// 创建printAge方法
val printAge: CtMethod = CtMethod.make("public void printAge(){System.out.println(age);}", person)
// 给printAge添加catch语句块,在kotlin中$需要转义
printAge.addCatch("{System.out.println(\$e);throw \$e;}", pool.get("java.lang.Exception"))
// 添加到person类中
person.addMethod(printAge)
效果如下:
public void printAge() {try {System.out.println(this.age);} catch (Exception var2) {System.out.println(var2);throw var2;}
}
2)$r的使用场景
例如给Person类添加一个convert方法
val convert = CtNewMethod.make("public int convert(){ Double d= 12.5;return (\$r)d;} ", person)
person.addMethod(convert)
效果如下:
public int convert() {double var1 = 12.5D;return (Integer)var1;
}
五、ProxyFactory的使用
通过ProxyFactory可以实现动态代理的方式在处理目标类的方法。假设要被代理的类定义如下:
package com.demo;public class PersonService {public int fly() {System.out.println("我飞起来了");return 0;}
}
通过ProxyFactory动态代理PersonService的fly方法,具体如下:
package com.demoimport javassist.ClassPool
import javassist.util.proxy.MethodHandler
import javassist.util.proxy.ProxyFactory
import javassist.util.proxy.ProxyObject/*** @author chenyousheng* @date 2022/2/13* @desc ProxyFactory动态代理目标类方法*/
object ProxyFactoryTest {@JvmStaticfun main(args: Array<String>) {// 获取ClassPoolval pool = ClassPool.getDefault()// 获取目标类val cc = pool.get("com.demo.PersonService")// 实例化代理类工厂val factory = ProxyFactory()// 设置代理类的父类,ProxyFactory将会动态生成一个类,继承该父类factory.superclass = cc.toClass()// 设置过滤器,判断哪些方法调用需要被拦截factory.setFilter {return@setFilter it.name == "fly"}// 创建代理类型val proxy = factory.createClass()// 创建代理实例,强转成父类val personService: PersonService = proxy.newInstance() as PersonService// 设置代理处理方法(personService as ProxyObject).handler = MethodHandler { self, thisMethod, proceed, args ->//thisMethod为被代理方法 proceed为代理方法 self为代理实例 args为方法参数println(thisMethod.name + "被调用前输出")try {val ret = proceed.invoke(self, *args)println(thisMethod.name + "正在调用,返回值: " + ret)return@MethodHandler ret} finally {println(thisMethod.name + "被调用后输出")}}// 调用代理类的fly方法personService.fly()}
}
输出结果如下:
fly被调用前输出
我飞起来了
fly正在调用,返回值: 0
fly被调用后输出
javassist使用指南相关推荐
- javassist编程指南(一)
javassist编程指南(主译) javassist是什么? Javassist(Java 编程辅助)使得Java字节码操作更简单. Javassist可用于编辑字节码的类库. 允许Java程序可以 ...
- Javassist 使用指南(一)
本文译自: Javassist Tutorial-1 原作者: Shigeru Chiba 完成时间:2016年11月 1. 读写字节码 我们知道 Java 字节码以二进制的形式存储在 class 文 ...
- Javassist使用指南1
1.创建了一个非默认的classpool,加入当前线程的上下文类加载器作为额外的类搜索路径 val classPool = ClassPool(false) classPool.appendClass ...
- javassist编程指南==读、写字节码
读.写字节码 Javassist是一个处理字节码的类库.Java字节码存储在一个叫做*.class的二进制文件中.每个class文件包含一个java类或者接口. javassist.CtClass代表 ...
- Javassist 使用指南
说明:翻译的太好,怕原文丢失就转载了. 1. 读写字节码 我们知道 Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口.Javaassist ...
- Javassist 使用指南 侵立删
1. 读写字节码 我们知道 Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口.Javaassist 就是一个用来处理 Java 字节码的类 ...
- Javassist 指南1
1.读写字节码 Javassist 是一个能处理 Java字节码 的类库,Java字节码存储在class文件中,每一个class文件都包含了一个Java类或一个接口类. 在Javassist中,使用J ...
- Java降落伞_javassist使用指南
Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含一个 Java 类或接口.Javaassist 就是一个用来 处理 Java 字节码的类库.它可以在一个已经编 ...
- Android 中使用Javassist
Javassist Javassist 是一个执行字节码操作的库.它可以在一个已经编译好的类中添加新的方法,或者是修改已有的方法,并且不需要对字节码方面有深入的了解. Javassist 可以绕过编译 ...
- Java字节码instrument研究
MyAgent项目 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="h ...
最新文章
- 【Python基础】Python的深浅拷贝讲解
- CSAPP实验二进制炸弹
- Vue源码后记-更多options参数(1)
- jdbc mysql 远程数据库_jdbc 连接远程mysql数据库的有关问题
- Class的getInterfaces与getGenericInterface区别
- 高并发场景下,如何保证生产者投递到消息中间件的消息不丢失?
- 判断字符是否包含有特殊字符
- 用idea创建vue项目
- 归并排序时间复杂度分析
- 关于web服务器硬件配置
- mysql允许连接表为空_mysql – 选择一个表中的所有项并与另一个表连接,允许空值...
- 给SpringBoot Web应用配上JavaFx漂亮衣服
- 第一次发,可能不太好,别喷我
- arduino智能跟随小车
- 快速求一个字符串的非空子串(不相同)的数量
- 英语十大词性之二 - 动词
- UE4学习(一)C++编程官方文档解读
- 【新书速递】图解IT-用Python轻松设计控制系统
- boston数据集预测房价
- CLRS第二章思考题
热门文章
- Foxpro 简体转繁体的一种方式(代码)
- 文件MD5查看linuxwindows
- 学校计算机和网络保密管理规定,计算机信息系统安全保密管理规定
- 基于CompactRIO的嵌入式车载电性能测试系统研发
- java与eclipse不匹配_【JAVA小白】 用eclipse输入格式不匹配的问题
- 国内第一款企业集中管理平台--极通EWEBS3.0
- Photoshop插件-删除亮调通道蒙板-脚本开发-PS插件
- 找不到好看的电影就看《IMDB排名前500电影》
- Redis入门指南笔记
- win10自带的打印机服务器,win10系统打印服务器安装设置的详细方法