一、前言
所谓的Javassist,其实就是如何生成一个Class文件或者修改一个Class文件的工具,包括对Class里的成员变量或者方法进行增加或修改。相比于ASM,Javassist最大的好处就是方便,简单,不用去关心字节码操作。
二、用Javassist生成文件
引入Javassist工具库
implementation 'org.javassist:javassist:3.28.0-GA'
首先,先简单生成一个Class文件。运行以下代码,就可以直接生成一个Class文件。
public class GenClass {
@Test
public static void main(String[] args) throws Exception {
//创建一个字节码池,用来存放生成的Class
ClassPool pool = ClassPool.getDefault();
// 创建一个User类
CtClass clz = pool.makeClass("cn.example.User");
//写入本地
clz.writeFile();
}
}
可以看到,在根目录下直接生成了一个User.Class文件。如下:
先解释一下上面代码的作用:
- 首先,ClassPool.getDefault(),用来存放我们生成的Class文件,把他加载进内存。
- 其次,pool.makeClass(),创建一个Class文件,它还有pool.get(),获取一个Class文件。
- 最后,clz.writeFile(),把Class文件输出出来。可传path,不传就默认输出到根目录。
很明显,如果我们想对Class进行自定义添加操作,那肯定是对clz对象进行操作,可以看看他有哪些方法,如下:
有了方法,那接下来我们看看如何给Class添加构造方法、成员变量、方法和接口。
2.1 添加成员变量
// 创建一个String变量name
CtField nameField = new CtField(pool.get("java.lang.String"), "name", clz);
// 设置name成员变量为私有属性,不设默认Public
nameField.setModifiers(Modifier.PRIVATE);
// 将name成员变量添加到Person类中
clz.addField(nameField);
// 创建一个Integer变量age
CtField ageField = new CtField(pool.get("java.lang.Integer"), "age", clz);
// 设置name成员变量为私有属性,不设默认Public
ageField.setModifiers(Modifier.PRIVATE);
// 将name成员变量添加到Person类中
clz.addField(ageField);
CtField方法参数解释:
/** * @param type 变量类型(String、Integer、double等,要写全路径) * @param name 变量名 * @param declaring 声明这个变量添加到哪个Class上 * */ public CtField(CtClass type, String name, CtClass declaring)
效果如下:
2.2 添加构造方法
// 添加一个无参数构造方法
CtConstructor defaultConstructor = new CtConstructor(new CtClass[]{}, clz);
// 设置方法体内容
defaultConstructor.setBody("{name = \"\";}");
clz.addConstructor(defaultConstructor);
// 添加一个有参数的构造方法
CtConstructor paramsConstructor = new CtConstructor(new CtClass[]{pool.get("java.lang.String")}, clz);
// 设置方法体内容 $0表示this,$1,$2...表示方法参数
paramsConstructor.setBody("{$0.name = $1;}");
clz.addConstructor(paramsConstructor);
CtConstructor方法参数解释:
/** * @param parameters 构造的参数类型(String、Integer、double等,要写全路径) * @param declaring 声明这个方法添加到哪个Class上 */ public CtConstructor(CtClass[] parameters, CtClass declaring)
效果如下:
2.3 添加方法
// 添加一个sayHello 无参的方法
CtMethod sayHello = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{}, clz);
//设置方法为PRIVATE,默认为PUBLIC
sayHello.setModifiers(Modifier.PRIVATE);
//设置方法体内容
sayHello.setBody("System.out.println(\"hello, this is \" + $0.name);");
clz.addMethod(sayHello);
// 添加一个sayHello 有参的方法
CtMethod sayHelloWithParam = new CtMethod(CtClass.voidType, "sayHello", new CtClass[]{pool.get("java.lang.String")}, clz);
//设置方法为PRIVATE,默认为PUBLIC
sayHelloWithParam.setModifiers(Modifier.PRIVATE);
//设置方法体内容
sayHelloWithParam.setBody("System.out.println(\"hello, this is \" + $1);");
clz.addMethod(sayHelloWithParam);
CtMethod方法参数解释:
/** * @param returnType 返回类型 * @param mname 方法名 * @param parameters 方法参数(String、Integer、double等,要写全路径) * @param declaring 声明这个方法添加到哪个Class上 */ public CtMethod(CtClass returnType, String mname, CtClass[] parameters, CtClass declaring)
效果如下:
PS:一个打印的是成员变量name,一个是传进来的变量
三、用Javassist修改Class文件
除了要如何生成一个Class文件外,我们还需要知道如何对一个已有的Class文件进行修改。
3.1 加载Class文件到内存
想要对Class进行操作前,那肯定需要将Class加载进内存,不然操作空气嘛😀。代码如下:
//创建一个字节码池,用来存放加载进来的Class
ClassPool pool = ClassPool.getDefault();
//添加Class路径,不能包括包名
pool.appendClassPath("D:\\20210426\\code\\otherCode\\GradlePluginDemo");
// pool.insertClassPath("D:\\20210426\\code\\otherCode\\GradlePluginDemo");
// 获取User.Class,不能加.class
CtClass clz = pool.get("cn.example.User");
//非常重要的一步,不是自己创建的Class,都需要先调用defrost,才可以进行修改Class。
clz.defrost();
PS:appendClassPath和insertClassPath区别
- 在ClassPool池中有一个搜索列表(链表结构),用来提供给pool.get获取class文件的来源。
- appendClassPath是添加到搜索列表最后。
- insertClassPath是添加到搜索列表最前面,如果先append,在insert,会把之前append的数据放在insert之后。
3.2 删除Class成员变量
删除名字是name的成员变量
for (CtField field : clz.getDeclaredFields()) {
System.out.println("field name:"+field.getName());
if (field.getName().equals("name")){
clz.removeField(field);
}
}
3.3 删除Class构造方法
删除名字是User的构造方法
for (CtConstructor constructor : clz.getDeclaredConstructors()) {
System.out.println("constructor name:"+constructor.getName());
if (constructor.getName().equals("User")){
clz.removeConstructor(constructor);
}
}
3.4 删除Class方法
删除名字是sayHello方法
for (CtMethod method : clz.getDeclaredMethods()) {
System.out.println("method name:"+method.getName());
if (method.getName().equals("sayHello")){
clz.removeMethod(method);
}
}
3.5 写回本地
修改完成别忘记写回本地,释放内存。当然,如果进程都结束了就没必要释放了。
//把修改的内容写入文件
clz.writeFile(fileName)
//释放内存
clz.detach()
四、如何测试?
看完了如何生成和修改一个Class文件,那怎么能快速的知道有没有生效呢?(如果你有好的方案欢迎评论)答案如下:
1.在Class类中添加一个main方法,main方法中调用sayHello方法(不需要测试方法的也可以不用调用)。
//添加一个main方法
CtMethod ctMethod = new CtMethod(CtClass.voidType, "main", new CtClass[]{pool.get(String[].class.getName())}, clz);
//将main方法声明为public static类型
ctMethod.setModifiers(Modifier.PUBLIC + Modifier.STATIC);
//设置方法体
ctMethod.setBody("{" +
"sayHello(\"hello, this is \");" +
"}");
clz.addMethod(ctMethod);
注意,main方法是static的,那sayHello也要设置成static,代码如下:
sayHelloWithParam.setModifiers(Modifier.PRIVATE+ Modifier.STATIC);
2. 把生成的Class文件实例化出来,genClass也就是上面生成的User.Class文件
public static void main(String[] args) throws Exception {
//测试
Class clazz = genClass();
Object obj = clazz.newInstance();
Method mainMethod = clazz.getMethod("main", new Class[]{String[].class});
mainMethod.invoke(obj, new String[1]);
}
3.运行代码
效果如下:
可根据自己想要测试的方法自行修改