Java Javassist

对 Java Javassist 使用的简单介绍

Javassist 简介

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

Javassist 是用于编辑(创建,修改).class字节码文件的 Java 库,一般情况下我们都是在.java文件中写代码,然后编译成.class文件,在加载进 Java 虚拟机中执行代码,如果要修改已编译好的文件,要使用010 Editor去手动的计算一些偏移值进行修改,但是 Javassist 的出现,使得我们操作.class文件变得简单,并且可以在 Java 虚拟机 JVM 运行时动态地改变.class文件

添加依赖:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>
</dependencies>

读取写入字节码

读写字节码

javassist.CtClass代表一个 class 文件的抽象类表示形式,一个CtClass(compile-time class 编译时的类)是一个处理 class 文件的句柄,以下是一个简单的程序:

1
2
3
4
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();

这段程序首先包含一个ClassPool对象,通过 javassist 控制字节码的修改。ClassPool对象是代表 class 文件的CtClass对象的容器。它根据构造一个CtClass对象的需求读取一个 class 文件,并记录被构建好的对象以供将来进行访问。 为了修改一个类的定义,用户必须首先从ClassPool对象的.get(className)方法获取一个CtClass引用。 在上述示例中,CtClass对象表示ClassPool中的类test.Rectangle,并且将其分配给变量ccClassPool对象由静态方法getDefault方法查找默认的系统检索 path 返回

从实现上来看,ClassPool是一个CtClass的哈希表,使用 class name 作为 key,ClassPool.get()方法通过检索这个哈希表找到一个CtClass对象关联指定的 key,如果CtClass对象没有找到,get()方法会读取 class 文件去构造一个CtClass对象,记录在哈希表中然后作为get()的返回值返回

ClassPool中获取到的CtClass对象是可以被修改的。在上述示例中,它被修改了,test.Rectangle的父类变更为test.Point,这个修改将会在最后CtClass.writeFile()方法调用后反映在 class 文件中,writeFile()方法将CtClass对象转换到 class 文件并且将其写入本地磁盘。Javassist 也提供了一个方法用于直接获取修改后的字节码toBytecode()

1
byte[] b = cc.toBytecode();

也可以像这样直接加载CtClass

1
Class clazz = cc.toClass();

toClass请求当前线程的上下文类加载器去加载 class 文件,返回一个java.lang.Class对象,实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;

import java.io.IOException;

public class Main {
public static void main(String[] args) {
// 获取 ClassPool
ClassPool pool = ClassPool.getDefault();

try {
// 通过 ClassPool 获取 CtClass
CtClass cc = pool.get("javaclass.Rectangle");
// 设置父类
cc.setSuperclass(pool.get("javaclass.Point"));
// 更新到 class 文件中
cc.writeFile();

// 获取修改后的字节码
// byte[] b = cc.toBytecode();
// System.out.println(new String(b));

// 加载类(请求当前线程的上下文加载器加载 CtClass 代表的类)
Class<?> clazz = cc.toClass();
System.out.println("superClass is : " + clazz.getSuperclass());
} catch (NotFoundException | CannotCompileException | IOException e) {
e.printStackTrace();
}
}
}

使用的两个类RectanglePoint不需要属性和方法,是两个空类

定义新的类

重新定义一个新的类,需要调用ClassPool.makeClass

1
2
3
4
5
6
7
8
9
10
11
12
import javassist.ClassPool;
import javassist.CtClass;

public class Main {
public static void main(String[] args) throws Exception {
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("test.Point");
// 输出 class test.Point
System.out.println(cc2.toClass());
}
}

这个程序定义了一个Point类,未包含任何成员,同样只作用于 JVM,成员方法可以通过使用CtClassaddMethod()方法传入一个CtMethod的工厂方法创建的对象作为参数来追加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class Main {
public static void main(String[] args) throws Exception {
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("test.make.Point");
// System.out.println(cc2.toClass().getMethods().length);
// 9

// 追加方法
cc2.addMethod(CtMethod.make(
"public void sayHello() {\n" +
" System.out.println(\"Hello!\");\n" +
"}",
cc2));
System.out.println(cc2.toClass().getMethods().length);
// 10
}
}

makeClass()方法不能创建一个新的接口,需要使用makeInterface()方法才可以。 接口中的成员方法可以通过CtMethodabstractMethod方法创建,还可以添加已存在的方法到新类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc1 = pool1.get("HelloWorld");
CtClass cc2 = pool1.makeClass("test.hello");

// 追加方法
CtMethod ctMethod = cc1.getDeclaredMethod("sayHello");
cc2.addMethod(new CtMethod(ctMethod, cc2, null));
// 保存为文件
cc2.writeFile();

MyClassLoader myClassLoader = new MyClassLoader();

byte[] bytes = cc2.toBytecode();
Class<?> clazz = myClassLoader.defineClass("test.hello", bytes);
Object object = clazz.newInstance();

try {
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(object);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

static class MyClassLoader extends ClassLoader {
Class<?> defineClass(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length);
}
}
}

自定义一个类加载器来加载我们的类

冻结类

如果一个CtClass对象通过writeFile()doBytecodetoClass方法被转换到 class 文件中,javassist 则会冻结这个CtClass对象。再对这个CtClass对象进行操作则会不允许,这在开发者他们尝试去修改一个已经被 JVM 加载过的 class 文件的时候会发出警告,因为 JVM 不允许重加载一个 class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class Main {
public static void main(String[] args) throws Exception {
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("test.make.Point");
// 下面的操作调用 toClass 使类被冻结
System.out.println(cc2.toClass().getMethods().length);
// 解冻
cc2.defrost();

// 追加方法
cc2.addMethod(CtMethod.make(
"public void sayHello() {\n" +
" System.out.println(\"Hello!\");\n" +
"}",
cc2));
cc2.writeFile();
// System.out.println(cc2.toClass().getMethods().length);
// attempted duplicate class definition for name: "test/make/Point"
System.out.println(cc2.getMethods().length);
}
}

修剪类

如果CtClass.prune()方法被调用,则 javassist 会在 CtClass 被冻结的时候(调用writeFile()doBytecodetoClass方法的时候)会修剪 CtClass 对象的数据结构。 为了降低内存消耗,修剪时会放弃对象中的不必要的属性。当一个 CtClass 对象被修剪后,方法的字节码则不能被访问除了方法名称、方法签名和注解。修剪过的 CtClass 对象不会被解冻。默认修剪值是 false:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class Main {
public static void main(String[] args) throws Exception {
// 定义一个新的类
ClassPool pool1 = ClassPool.getDefault();
CtClass cc2 = pool1.makeClass("test.make.Point");

// 追加方法
cc2.addMethod(CtMethod.make(
"public void sayHello() {\n" +
" System.out.println(\"Hello!\");\n" +
"}",
cc2));

cc2.prune();
// cc2.writeFile();
// test.make.Point class is frozen and pruned

System.out.println(cc2);
// javassist.CtNewClass@3d71d552[changed frozen pruned public class test.make.Point fields= constructors= methods=javassist.CtMethod@9798dee8[public sayHello ()V], ]
}
}

禁止修剪stopPruning(true),必须在对象的前面调用:

1
2
3
4
5
CtClasss cc = ...;
// 前面调用禁止修剪
cc.stopPruning(true);
...
cc.writeFile();

类路径

默认的ClassPool.getDefault()检索路径和 JVM 底层路径一致(classpath),添加类路径:

1
2
// 添加 class 查找路径
pool.insertClassPath(...);

更多内容参考:读、写字节码

ClassPool

一个ClassPool对象是包含CtClass对象的容器。一旦一个CtClass对象被创建后,就会被记录到一个ClassPool中。这是因为编译器在编译源码时会引用代表CtClass的类,可能会访问CtClass对象

比如,假设一个新的方法getter()被添加到一个代表Point类的CtClass对象中。之后,程序尝试编译Point中包含调用getter()方法的源代码,并且使用编译后的代码作为方法的方法体,将其添加到另一个类Line中。如果代表PointCtClass对象丢失了,编译器则不能编译Line中调用getter()的方法。注意:原来定义的类是不包含getter()方法的。因此,为了正确编译这样的方法调用,ClassPool必须包含程序执行时所有的CtClass实例

移除 CtClass

可以调用CtClassdetach()方法,然后会将该对象从ClassPool中移除:

1
2
3
4
5
ClassPool classPool = ClassPool.getDefault();
CtClass cc = classPool.get("org.byron4j.cookbook.javaagent.Javassist2ClassPool");

// 调用该方法后,会将 CtClass 对象从 ClassPool 中移除
cc.detach();

级联 ClassPool

多个 ClassPool 对象可以像java.lang.ClassLoader一样级联:

1
2
3
4
// 级联 ClassPool
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");

如果调用了child.get()方法,child 首先委托父 ClassPool,如果父 ClassPool 加载 class 文件失败,然后 child 再尝试从./classes/目录下查找类文件,如果child.childFirstLookup设置为 true,则 child 尝试在委托给父 classPool 之前去加载 class 文件

更改类名

一个新的 class 可以被定义为一个已存在的类的副本:

1
2
3
4
5
// 首先获取表示 Point 类的一个 CtClass 对象
// 然后调用 setName 修改类名
ClassPool pool3 = ClassPool.getDefault();
CtClass cc3 = pool3.get("org.byron4j.cookbook.javaagent.Point");
cc3.setName("Pair");

这个程序首先包含类 Point 的 ctClass 对象,然后调用setName()方法为 CtClass 对象设置新的名称。在这个调用之后,所有由该 CtClass 对象定义的所有类名展示均由 Point 变更为 Pair,类定义的其他部分则不会变更

为了创建一个默认 ClassPool 实例(Clas.getDefault()返回的)的一个副本,可以使用以下代码片段:

1
2
3
4
5
6
ClassPool pool10 = ClassPool.getDefault();
CtClass ctClass10 = pool10.get("org.byron4j.cookbook.javaagent.Point");
ClassPool pool20 = new ClassPool(true);
CtClass ctClass20 = pool20.get("org.byron4j.cookbook.javaagent.Point");
System.out.println(pool10 == pool20);
// false 不同的 ClassPool 中表示同一个类的 CtClass 对象

更多内容参考:ClassPool 类池

Class Loader

toClass 方法

CtClass提供了一个便捷的方法toClass,请求当前线程的类加载器去加载 CtClass 表示的类。调用此方法必须具有适应的权限,否则会抛出一个SecurityException异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javaclass.Javassist3ClassLoader;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class Main {
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass cc = cp.get("javaclass.Javassist3ClassLoader");
CtMethod m = cc.getDeclaredMethod("say");

m.insertBefore("System.out.println(\"Ready to say: \");");
/*
Ready to say:
Hello!
*/

Class c = cc.toClass();
Javassist3ClassLoader javassist3ClassLoader = (Javassist3ClassLoader)c.newInstance();
javassist3ClassLoader.say();
}
}

首先,通过测试类的表示Javassist3ClassLoader类的CtClass修改其say方法,在方法体前面增加一行输出语句; 然后,通过CtClass的 toClass 方法请求当前线程(Javassist3ClassLoaderTest 类所在的线程)去加载 Javassist3ClassLoader 类; 最后,通过 Class 对象的静态方法 newInstance 构造一个 Javassist3ClassLoader 对象,并调用其 say 方法,得到字节码修改后的方法执行内容结果

注意: 上面的程序依赖于 Javassist3ClassLoaderTest 类所在的类加载器在调用toClass之前没有加载过 Javassist3ClassLoader 类

如果程序运行在 Web 容器中例如 JBoss、Tomcat 中, 上下文的类加载器使用toClass()方法可能并不适当。在这种情况下,你可能会看到一个不期望的异常ClassCastException。为了避免这种情况,你必须明白清楚地给定一个适当的类加载器给toClass方法。例如,如果bean是你的会话的 bean 对象:

1
2
CtClass cc = ...
Class c = cc.toClass(bean.getClass().getClassLoader());

Javassist Loader

javassist 提供了一个类加载器javassist.Loader,这个类加载器使用javassist.ClassPool对象读取 class 文件,例如,javassist.Loader可用于使用 javassist 修改的指定的类:

1
2
3
4
5
6
7
8
9
ClassPool pool = ClassPool.getDefault();
// 使用 ClassPool 创建 Loader
Loader cl = new Loader(pool);

CtClass ct = pool.get("org.byron4j.cookbook.javaagent.Rectangle");
ct.setSuperclass(pool.get("org.byron4j.cookbook.javaagent.Point"));

Class<?> c = cl.loadClass("org.byron4j.cookbook.javaagent.Rectangle");
Object o = c.newInstance();

这个程序修改了类 Rectangle 类,将其父类设置为 Point 类,然后程序加载了修改后的 Rectangle 类,并且创建了一个实例

更多内容参考:Class loader 类加载