研究 Javassist 的起因是维护的项目是从外部采购的一个系统,有几个核心的类在构造函数里从数据库加载一些配置信息,而这些配置信息基本是不会改变的,应当缓存起来。这些类没有源码,class 文件还是混淆过的。
想来想去,觉得用字节码操作工具来改写是比较合适的,把改写后生成的 class 文件替换原来的,这样使用这些类的地方也不用做任何修改。
1. Javassist 简介
Javassist 是一个字节码操作库,通过它,可以在运行时改写类:添加新的字段、方法和构造函数,改变类、父类和接口的方法。
Javassist 定义了 CtField, CtMethod, CtConstructor, CtClass 来表示 字段、方法、构造函数、类。
2. 操作示例
2.1 操作目标代码
定义一个要操作的目标类,编译生成 class 文件。
package net.coderbee.javassist;
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("name:" + this.name + ", age:" + this.age);
}
public String show() {
String msg = "show name:" + this.name + ", age:" + this.age;
System.out.println(msg);
return msg;
}
public int longRun(long sleep) throws InterruptedException {
Thread.sleep(sleep);
return 0;
}
}
2.2 获取 CtClass 实例
获取 CtClass 实例的方式非常简单:ClassPool.getDefault().get("net.coderbee.javassist.Person");
。
2.3 操作字段
如下代码获取字段名为 name
的字段,把它改为 public
且是 volatile
修饰:
CtField nameField = ctClass.getField("name");
nameField.setModifiers(Modifier.PUBLIC | Modifier.VOLATILE);
2.4 操作方法
Javassist 用 $
加数字的标识符表示 方法或构造函数的 参数。 用 $0
表示当前类的实例 this
, $$
表示当前方法的一序列参数, $1
表示第一个参数,$2
表示第二个参数,其他参数类推。
2.4.1 添加构造函数
下面的示例代码给 Person
类添加另一个构造函数:
CtClass stringClass = ClassPool.getDefault().get("java.lang.String");
CtClass intClass = ClassPool.getDefault().getCtClass("int");
CtConstructor constructor = new CtConstructor(new CtClass[] { intClass, stringClass }, ctClass);
constructor.setBody("{this.age = $0; this.name = $2; }");
ctClass.addConstructor(constructor);
2.4.2 给方法加上耗时统计
private static void wrapMethod(CtClass ctClass, String methodName) throws NotFoundException, CannotCompileException {
CtMethod targetMethod = ctClass.getDeclaredMethod(methodName);
String returnType = targetMethod.getReturnType().getName();
String newMethodName = targetMethod.getName() + "$impl";
CtMethod cloneMethod = CtNewMethod.copy(targetMethod, newMethodName, ctClass, null);
cloneMethod.setModifiers(Modifier.PRIVATE);
ctClass.addMethod(cloneMethod);
String warpBody = buildWrapBody(returnType, newMethodName);
targetMethod.setBody(warpBody);
}
private static String buildWrapBody(String returnType, String newMethodName) {
boolean hasReturn = !"void".equals(returnType);
StringBuilder sb = new StringBuilder(256);
sb.append("\t{\r\n\t\tlong start = System.currentTimeMillis();\r\n");
if (hasReturn) {
sb.append(returnType).append("\t\tresult = ").append(newMethodName).append("($$);");
} else {
sb.append("\r\n\t\t").append(newMethodName).append("($$);");
}
sb.append("\r\n\t\tlong end = System.currentTimeMillis();");
sb.append("\r\n\t\tSystem.out.println(\"call show used millis:\" + (end-start));");
if (hasReturn) {
sb.append("\r\n\t\treturn result;");
}
sb.append("\r\n\t}");
return sb.toString();
}
上面的代码处理过后生成的字节码文件反编译后是这样的:
package net.coderbee.javassist;
import java.io.PrintStream;
public class Person {
public volatile String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
System.out.println("name:" + this.name + ", age:" + this.age);
}
public String show() {
long l1 = System.currentTimeMillis();
String str = show$impl();
long l2 = System.currentTimeMillis();
System.out.println("call show used millis:" + (l2 - l1));
return str;
}
public int longRun(long paramLong) throws InterruptedException {
long l1 = System.currentTimeMillis();
int i = longRun$impl(paramLong);
long l2 = System.currentTimeMillis();
System.out.println("call show used millis:" + (l2 - l1));
return i;
}
private String show$impl() {
String msg = "show name:" + this.name + ", age:" + this.age;
System.out.println(msg);
return msg;
}
private int longRun$impl(long sleep) throws InterruptedException {
Thread.sleep(sleep);
return 0;
}
public Person(int paramInt, String paramString) {
this(paramString, paramInt);
this.age = paramInt;
this.name = paramString;
}
}
有个小问题是,Javassist 自动加了一个没有使用的导入: import java.io.PrintStream;
。
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。