javassist 通过修改字节码在运行时或编译时动态生成和修改 java 类,核心做法是获取 ctclass 对象后调用其方法添加字段、方法或修改逻辑,最后写入文件或用 ctclass.toclass() 加载到 jvm。使用它需要引入 javassist.jar 依赖,操作时注意处理 cannotcompileexception 和 notfoundexception,并在修改完成后调用 detach() 释放资源以避免内存泄漏。

如何动态向现有类添加新方法
要动态向一个已存在的类添加新方法,可以使用 CtClass 的 addMethod() 函数。首先需要获取目标类的 CtClass 实例,然后创建一个 CtMethod 对象并设置其签名和代码体,最后将该方法添加到类中。这种方法常用于 AOP 框架或测试 Mock,无需修改源码即可增强类功能。

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
public class AddMethodExample {
public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, IllegalArgumentException, java.lang.InstantiationException {
// 1. 创建类池
ClassPool pool = ClassPool.getDefault();
// 2. 获取目标类 (这里以 java.util.Date 为例,实际项目中通常是自定义类)
CtClass ctClass = pool.get("java.util.Date");
// 3. 定义新方法: public String getYearInfo() { return "Current Year: " + this.getYear(); }
CtMethod newMethod = new CtMethod(
pool.get("java.lang.String"), // 返回类型
"getYearInfo", // 方法名
new javassist.CtClass[]{}, // 参数列表 (无参)
ctClass // 所属类
);
// 4. 设置方法体
newMethod.setBody("{ return \"Current Year: \" + this.getYear(); }");
// 5. 将方法添加到类中
ctClass.addMethod(newMethod);
// 6. 加载并实例化测试
Class> loadedClass = ctClass.toClass();
Object instance = loadedClass.newInstance();
// 反射调用新方法
java.lang.reflect.Method method = loadedClass.getMethod("getYearInfo");
System.out.println(method.invoke(instance));
// 7. 释放资源
ctClass.detach();
}
}
上述代码展示了完整的流程:首先通过 ClassPool.getDefault() 获取类池,然后定位到目标类。接着构造 CtMethod,利用 setBody() 传入字符串形式的 Java 代码。执行 toClass() 后,JVM 会重新定义该类,此时可以通过反射调用新增的方法。务必记得调用 detach() 以防止永久代或元空间溢出。
如何在运行时修改已有方法的实现逻辑
若需修改已有方法的逻辑,应使用 CtMethod 的 insertBefore()、insertAfter() 或 setBody() 方法。这些方法允许你在原方法执行前后插入代码,或者直接替换整个方法体。这在实现日志记录、权限检查或性能监控时非常有用,能够实现对原有业务逻辑的非侵入式增强。

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.NotFoundException;
public class ModifyMethodExample {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("com.example.MyService");
// 创建一个简单的原始方法
CtMethod originalMethod = new CtMethod(
pool.get("int.class"),
"calculate",
new CtClass[]{pool.get("int.class")},
ctClass
);
originalMethod.setBody("{ return $1 * 2; }"); // 原始逻辑: 乘以2
ctClass.addMethod(originalMethod);
// 修改逻辑: 在调用前打印日志,并改变返回值逻辑
// 使用 insertBefore 插入前置代码
originalMethod.insertBefore("System.out.println(\"Calculating with input: \" + $1);");
// 使用 setBody 完全替换逻辑,例如改为乘以3
originalMethod.setBody("{ return $1 * 3; }");
// 加载并测试
Class> clazz = ctClass.toClass();
Object instance = clazz.getDeclaredConstructor().newInstance();
java.lang.reflect.Method m = clazz.getMethod("calculate", int.class);
Integer result = (Integer) m.invoke(instance, 5);
System.out.println("Result: " + result); // 预期输出: Calculating... Result: 15
ctClass.detach();
}
}
此示例演示了如何先构建一个基础类和方法,然后通过 insertBefore() 注入日志逻辑,并通过 setBody() 覆盖原有计算逻辑。$1 代表第一个参数,这种语法是 Javassist 的特有简写。最终加载的类将包含新的行为。这种方式比继承或代理更直接,但需注意方法签名的匹配和异常处理。
如何处理 Javassist 中的变量引用与特殊符号
在使用 Javassist 编写方法体时,必须正确处理局部变量和参数的引用。Javassist 使用 $1, $2 等表示方法参数,$0 表示当前实例(即 this),$cflow 用于控制流分析。如果直接在字符串中使用 Java 关键字或特殊字符,需要进行转义。理解这些符号是编写正确动态代码的关键,否则会导致 CannotCompileException。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











