Javassist 学习
环境搭建
这个非常简单,只需要你自己加一个依赖
<!-- https://mvnrepository.com/artifact/javassist/javassist -->
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency>
简单了解
Java programming ASSISTant,Java编程助手。是Java中编辑字节码的类库,它使Java程序能够在运行时定义一个新类,并在JVM加载时修改类文件。
Java中所有的类都被编译为class文件来运行,在编译完class文件之后,类不能再被显示修改,而Javassist就是用来处理编译后的class文件,它可以用来修改方法或者新增方法,并且不需要深入了解字节码,还可以生成一个新的类对象。
ClassPool这个类是javassist的核心组件之一。ClassPool是CtClass对象容器,CtClass对象必须从该对象获得。
一些基础的语法
从上文的demo中可以看到部分使用方法,在javassist中CtClass代表的就是类class,ClassPool就是CtClass的容器,ClassPool维护了所有创建的CtClass对象,需要注意的是当CtClass数量过大会占用大量内存,需要调用CtClass.detach()释放内存。
ClassPool重点有以下几个方法:
- getDefault() 单例获取ClassPool
- appendClassPath() 将目录添加到ClassPath
- insertClassPath() 在ClassPath插入jar
- get() 根据名称获取CtClass对象
- toClass() 将CtClass转为Class 一旦被转换则不能修改
- makeClass() 创建新的类或接口
更多移步官方文档:http://www.javassist.org/html/javassist/ClassPool.html
CtClass需要关注的方法:
- addConstructor() 添加构造函数
- addField() 添加字段
- addInterface() 添加接口
- addMethod() 添加方法
- freeze() 冻结类使其不能被修改
- defrost() 解冻使其能被修改
- detach() 从ClassPool中删除类
- toBytecode() 转字节码
- toClass() 转Class对象
- writeFile() 写入.class文件
- setModifiers() 设置修饰符
移步:http://www.javassist.org/html/javassist/CtClass.html
CtMethod继承CtBehavior,需要关注的方法:
- insertBefore 在方法的起始位置插入代码
- insterAfter 在方法的所有 return 语句前插入代码
- insertAt 在指定的位置插入代码
- setBody 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除
- make 创建一个新的方法
简单使用
创建class
package javassist;
import java.io.IOException;
public class Create {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("javassist.Person");
// 添加字段
CtField name = new CtField(pool.get("java.lang.String"), "name", cc);
name.setModifiers(Modifier.PRIVATE);
cc.addField(name, CtField.Initializer.constant("LJL"));
CtField age = new CtField(pool.get("int"), "age", cc);
age.setModifiers(Modifier.PRIVATE);
cc.addField(age, CtField.Initializer.constant(18));
// 添加getter和setter方法
cc.addMethod(CtNewMethod.getter("getName", name));
cc.addMethod(CtNewMethod.getter("getAge", age));
cc.addMethod(CtNewMethod.setter("setName", name));
cc.addMethod(CtNewMethod.setter("setAge", age));
// 添加构造函数
CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
cons.setBody("{name = \"nn0nkey\";}");
cc.addConstructor(cons);
CtConstructor cons1 = new CtConstructor(new CtClass[]{pool.get("java.lang.String"), pool.get("int")}, cc);
cons1.setBody("{name = $1; age = $2;}");
cc.addConstructor(cons1);
// 添加toString方法
CtMethod toString = new CtMethod(pool.get("java.lang.String"), "toString", new CtClass[]{}, cc);
toString.setModifiers(Modifier.PUBLIC);
toString.setBody("{return \"name: \" + name + \", age: \" + age;}");
cc.addMethod(toString);
// 将类写入文件
cc.writeFile("F:\\IntelliJ IDEA 2023.3.2\\java脚本\\CC1_5_6_7\\target\\classes");
}
}
需要注意的是在setBody()中我们使用了$
符号代表参数
// $0代表this $1代表第一个传入的参数 类推
printName.setBody("{System.out.println($0.name);}");
body中代码该怎么写就怎么写
我们运行就可以生成我们的class文件
还是非常完美的
使用CtClass对象
这里一共有三种方法
反射调用
public class Useit {
public static void main(String[] args) throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
ClassPool classPool=ClassPool.getDefault();
CtClass cc=classPool.get("javassist.Person");
Object person = cc.toClass().newInstance();
Method setName=person.getClass().getDeclaredMethod("setName",String.class);
setName.setAccessible(true);
setName.invoke(person,"k1n9");
Method tostring=person.getClass().getMethod("toString");
Object result = tostring.invoke(person);
System.out.println(result);
}
}
看代码就很容易看明白就是获取class后实例化,然后步骤就和反射一样了
加载class文件
这个和上面只能说大差不差,就是修改了获取class的方法
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("F:\\IntelliJ IDEA 2023.3.2\\java脚本\\CC1_5_6_7\\target\\classes");
// 获取上面生成的类
CtClass ctClass = pool.get("javassist.Person");
Object person = ctClass.toClass().newInstance();
// ...... 下面和通过反射的方式一样去使用
需要注意的点是appendClassPath中填入目录,不要具体的文件
通过接口的方式
这个方法是最方便的,因为反射确实太麻烦;我们考虑为该类写一个接口
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package javassist;
public interface PersonI {
void setName(String var1);
void setAge(int var1);
String getName();
int getAge();
String toString();
}
然后我们只需要为我们的类实现这个接口,就可以直接调用这个方法了
ClassPool pool = ClassPool.getDefault();
pool.appendClassPath("F:\\IntelliJ IDEA 2023.3.2\\java脚本\\CC1_5_6_7\\target\\classes");
// 获取接口
CtClass codeClassI = pool.get("javassist.PersonI");
// 获取上面生成的类
CtClass ctClass = pool.get("javassist.Person");
// 使代码生成的类,实现 PersonI 接口
ctClass.setInterfaces(new CtClass[]{codeClassI});
// 以下通过接口直接调用 强转
PersonI person = (PersonI)ctClass.toClass().newInstance();
System.out.println(person.getName());
person.setName("llll");
System.out.println(person.toString());
需要注意的就是一定要强转
修改现有的类对象
这个也是我们在实战中比较好用的一个方法
比如我们修改我们的toString方法
package javassist;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.CannotCompileException;
import java.io.IOException;
public class Refresh {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IOException {
ClassPool pool = ClassPool.getDefault();
// 获取 Person 类
CtClass person = pool.getCtClass("javassist.Person");
// 获取 toString 方法
CtMethod toString =new CtMethod(pool.get("java.lang.String"), "toString", new CtClass[]{}, person);
// 修改 toString 方法的函数体
toString.setBody("{return \"Are you joker?\";}");
// 将修改后的类写入文件(可选)
person.writeFile("F:\\IntelliJ IDEA 2023.3.2\\java脚本\\CC1_5_6_7\\target\\classes");
}
}
也是成功了好吧