大蜕

Something About Javassist

Javassist(Java编程助手)简化了Java字节码的操作。它是一个用于编辑Java字节码的类库;它允许Java程序在运行时定义新类,并在JVM加载类文件时对其进行修改。 与其他类似的字节码编辑器不同,Javassist提供了两个级别的API:源代码级别和字节码级别。如果用户使用源代码级别API,他们无需了解Java字节码的规范即可编辑类文件。 整个API仅使用Java语言的词汇表设计。您甚至可以以源代码文本的形式指定要插入的字节码;Javassist会实时编译它。另一方面,字节码级别API允许用户像其他编辑器一样直接编辑类文件。

读取和写入字节码

获取 CtClass 对象:

// 从类池中获取
final ClassPool classPool = new ClassPool(true);
//通过系统classloader加载类
classPool.appendClassPath(new LoaderClassPath(ClassLoader.getSystemClassLoader()));
//通过自定义classloader加载类
classPool.appendClassPath(new LoaderClassPath(loader));

修改类定义:

CtClass cc = xxx;
CtField f = new CtField(CtClass.intType, "newField", cc);
cc.addField(f);

ClassPool 的层次结构:

ClassPool 可以组织成层次结构。如果子 ClassPool 在本地找不到类文件,它可以委托父 ClassPool 查找。这种层次结构对于共享类定义很有用。 例如,你可以为每个类加载器创建一个子 ClassPool,并让它们共享一个公共的父 ClassPool。这样,不同类加载器加载的类可以共享相同的 CtClass 对象。

类加载器

CtClass.toClass() 方法

CtClass 的 toClass() 方法将 CtClass 对象转换为 java.lang.Class 对象。转换后的 Class 对象可以由 Java 应用程序使用。 但是,toClass() 要求调用者的类加载器必须能够加载 CtClass 对象所表示的类。如果调用者的类加载器无法加载该类,则会抛出 ClassNotFoundException。

val instance = ctClass.toClass().getDeclaredConstructor().newInstance();

自定义类加载器:

如果需要更复杂的类加载行为,可以定义自己的类加载器, 例如:

public class MyLoader extends ClassLoader {
    private ClassPool pool;

    public MyLoader(ClassPool pool) {
        this.pool = pool;
    }

    protected Class findClass(String name) throws ClassNotFoundException {
        CtClass cc = pool.get(name);
        // 修改 cc...
        byte[] b = cc.toBytecode();
        return defineClass(name, b, 0, b.length);
    }
}

定制

在方法体开头/结尾插入代码:

CtMethod 和 CtConstructor 提供了 insertBefore()、insertAfter() 和 addCatch() 方法,用于在方法体中插入代码。这些方法接受一个表示 Java 代码块的字符串参数。

CtMethod m = xxx;
m.insertBefore("{ System.out.println(\"start\"); }");

插入的代码块可以访问方法的局部变量和参数。

添加新方法

CtClass 的 addMethod() 方法向类中添加一个新方法。例如:

CtClass cc = xxx;
CtMethod newMethod = CtNewMethod.make(
    "public int newMethod(int x) { return x * 2; }",
    cc);
cc.addMethod(newMethod);

递归方法

Javassist 支持递归方法调用。但是,如果递归方法调用自身,则插入的代码也会被递归调用,这可能导致无限循环。为了避免这种情况,你可以使用 $0、$1、$2 等来引用方法的参数,而不是使用方法名。例如:

m.insertBefore("{ System.out.println($1); }");

$1 表示方法的第一个参数。 注: Javassist 可以用于在方法调用前后插入代码,这在 J2EE 中用于实现拦截器。

生成新类和方法

ClassPool.makeClass() 方法创建一个新的类。例如:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Apple");

然后你可以使用 CtClass 的方法向类中添加字段和方法:

CtMethod m = CtNewMethod.make(
    "public String getColor() { return color; }",
    cc);
cc.addMethod(m);

调试 Javassist

如果插入的代码无法编译,Javassist 会抛出 CannotCompileException。你可以通过调用 CannotCompileException.getReason() 来获取编译错误的原因。 此外,可以通过调用 CtMethod 的 instrument() 方法来检查插入的代码的字节码。

Reference:

https://www.javassist.org/