Javassist 字节码操作库

研究 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笔记,可以更及时回复你的讨论。