Java Javassist
概述
Java programming ASSISTant,Java编程助手。是Java中编辑字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。
Java中所有的类都被编译为class文件来运行,在编译完class文件之后,类不能再被显式修改,而Javassist就是用来处理编译后的class文件,它可以用来修改方法或者新增方法,并且不需要深入了解字节码,还可以生成一个新的类对象。
Javassist核心API
ClassPool
这个类是javassist的核心组件之一。ClassPool是CtClass对象容器,
常用方法:
ClassPool getDefault()
:返回默认的ClassPool,一般通过该方法创建我们的ClassPool;
ClassPool insertClassPath(ClassPath cp)
:将一个ClassPath对象插入到类搜索路径的起始位置,也就是向ClassPool容器插入一个.class
对象。
ClassPool appendClassPath
:将一个ClassPath对象加到类搜索路径的末尾位置;
CtClass makeClass(java.lang.String classname)
:根据类名创建新的CtClass对象。类名必须是全量类名。
CtClass get(java.lang.String classname)
:从源中读取类文件,并返回对CtClass来表示对该类文件的对象的引用。
CtClass
在javassist中每个需要编译的class都对应一个CtClass实例,CtClass(compile time class),这些类会存储在ClassPool中。所以CtClass对象必须从该对象容器中获取。
常用方法:
void setSuperclass(CtClass clazz)
:更改超类(父类),除非此对象表示接口。
byte[] toBytecode()
:将该类转换为类文件,即将CtClass对象cc转换为字节码数组;
CtConstructor makeClassInitializer()
:制作一个空的类初始化程序(静态构造函数)。
Class x.toClass()
:将Ctclass类型的字节码转换成Class类型。
CtMethod/CtField
其实这三个可以理解为加强版Class/method/field对象。同样可以使用CtClass中的CtField和CtMethod来获取类对象中的字段和方法。
Maven
在一个项目中,想要使用就需要加依赖,
在POM.XML中添加如下代码即可(注意依赖的版本):
1
2
3
4
5
6
7
|
//这样才能使用javassist
<!-- https://mvnrepository.com/artifact/org.javassist/javassist -->
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>
|
读取类/成员变量/方法信息的代码
使用ClassPool对象获取到CtClass对象后就可以像使用Java反射API一样去读取类信息了。最终在maven项目中的测试代码如下:
Test.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package org.example;
public class Test{
private String name;
protected char yn;
public int age;
public void setAge(int age){
this.age = age;
}
protected int getAge(){
return this.age;
}
public Test(String name,char yn,int age){
this.age = age;
this.yn = yn;
this.name = name;
}
private String getName(){
return this.name;
}
}
|
获取的操作:
Main.java
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
|
package org.example;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtConstructor;
public class Main{
public static void main(String[] args) throws Exception{
//获取ClassPool对象
ClassPool classPool = ClassPool.getDefault();
System.out.println("1:"+classPool);
//获取CtClass对象
CtClass ctClass = classPool.getCtClass("org.example.Test");//这里get()等同于getClass()
System.out.println("2:"+ctClass);
//获取CtField属性
CtField[] ctField = ctClass.getDeclaredFields();
for(CtField x : ctField){
System.out.println("3:"+x);
}
//获取CtMethod方法
CtMethod[] ctMethod = ctClass.getDeclaredMethods();
for(CtMethod x : ctMethod){
System.out.println("4:"+x);
}
//获取CtConStructor构造方法
CtClass[] parameters = new CtClass[]{
classPool.get("java.lang.String"),
CtClass.charType,
CtClass.intType
};
CtConstructor ctConstructor = ctClass.getDeclaredConstructor(parameters);
System.out.println("5:"+ctConstructor);
}
}
|
输出为:
1
2
3
4
5
6
7
8
9
|
1:[class path: java.lang.Object.class;]
2:javassist.CtClassType@5caf905d[public class org.example.Test fields=org.example.Test.name:Ljava/lang/String;, org.example.Test.yn:C, org.example.Test.age:I, constructors=javassist.CtConstructor@3d494fbf[public Test (Ljava/lang/String;CI)V], methods=javassist.CtMethod@3ac68cb[public setAge (I)V], javassist.CtMethod@7424e08a[protected getAge ()I], javassist.CtMethod@26562bc2[private getName ()Ljava/lang/String;], ]
3:org.example.Test.name:Ljava/lang/String;
3:org.example.Test.yn:C
3:org.example.Test.age:I
4:javassist.CtMethod@3ac68cb[public setAge (I)V]
4:javassist.CtMethod@7424e08a[protected getAge ()I]
4:javassist.CtMethod@26562bc2[private getName ()Ljava/lang/String;]
5:javassist.CtConstructor@3d494fbf[public Test (Ljava/lang/String;CI)V]
|
注意看读取代码这里的细节。与反射对比,尤其是对于函数参数类型的改变。
修改类方法
只需要调用CtMethod类的对应的API,CtMethod提供了类方法修改的API,如:
Test.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package org.example;
public class Test{
private String name;
protected char yn;
public int age;
public void setAge(int age){
this.age = age;
}
protected int getAge(){
return this.age;
}
public Test(String name,char yn,int age){
this.age = age;
this.yn = yn;
this.name = name;
}
private String getName(){
return this.name;
}
}
|
Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package org.example;
import javassist.ClassPool;
import javassist.CtMethod;
import javassist.CtClass;
public class Main{
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.getCtClass("org.example.Test");
//修改整个代码块
CtMethod ctMethod = ctClass.getDeclaredMethod("getAge");
ctMethod.setBody("{return \"haha\" ;}");
//修改部分,看代码结构
CtMethod ctMethod1 = ctClass.getDeclaredMethod("setAge",new CtClass[]{CtClass.intType});
ctMethod1.insertBefore("System.out.println(\"before is\");");
CtMethod ctMethod2 = ctClass.getDeclaredMethod("getName");
ctMethod2.insertAfter("System.out.println(\"after is\");");
//输出修改后的字节码到文件,方便看结果
ctClass.writeFile("output");//落地的是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
|
//Test.class
package org.example;
public class Test {
private String name;
protected char yn;
public int age;
public void setAge(int age) {
System.out.println("before is");
this.age = age;
}
protected int getAge() {
return (int)"haha";
}
public Test(String name, char yn, int age) {
this.age = age;
this.yn = yn;
this.name = name;
}
private String getName() {
String var2 = this.name;
System.out.println("after is");
return var2;
}
}
|
可以对比一下之前的Test.java看看结果。
动态创建一个类
API提供相应的make方法实现的操作
看下面的代码:
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
|
package org.example;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
public class Main {
public static void main(String[] args) {
try {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
// 使用ClassPool创建一个新的类
CtClass ctClass = classPool.makeClass("org.example.haha");
// 创建类成员变量content
CtField ctField = CtField.make("private static String content = \"Hello world~\";", ctClass);
// 将成员变量添加到ctClass对象中
ctClass.addField(ctField);
// 创建一个主方法并输出content对象值
CtMethod ctMethod = CtMethod.make(
"public static void main(String[] args) { System.out.println(content); }", ctClass
);
// 将成员方法添加到ctClass对象中
ctClass.addMethod(ctMethod);
//根据包结构创建目录并生成文件
ctClass.writeFile("output");
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
很清楚,理解学习一下代码就行了,建议在学习后自己敲一遍。
随后就生成了haha.class

内容为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.example;
public class haha {
private static String content = "Hello world~";
public static void main(String[] var0) {
System.out.println(content);
}
public haha() {
}
}
|
一般具体使用的时候会利用到static语句块,简单构造一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package java_foundation;
import javassist.*;
public class Main {
public static void main(String[] args) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("haha");
CtField ctField = CtField.make("private String content = \"111\";", ctClass);
ctClass.addField(ctField);
CtMethod ctMethod = CtMethod.make("public String getContent(){ return this.content;}", ctClass);
ctClass.addMethod(ctMethod);
CtConstructor ctConstructor = ctClass.makeClassInitializer();
ctConstructor.insertBefore("System.out.println(\"123\");");
ctClass.writeFile("output");
Class clazz = ctClass.toClass();
clazz.getDeclaredConstructor().newInstance();
}
}
|
成功在公职太输出 123。
生成的haha.class文件内容为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class haha {
private String content = "111";
public String getContent() {
return this.content;
}
static {
System.out.println("123");
}
public haha() {
}
}
|
其实还有其他的API可以用来构造一个类以及类中的各种属性,但这里就不多说了。
也可以利用二进制来动态创建,如下:
但是这个需要在项目中添加依赖
1
2
3
4
5
|
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
|
然后再运行下面这个代码即可
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
package org.example;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
try {
// 创建ClassPool对象
ClassPool classPool = ClassPool.getDefault();
// 使用ClassPool创建一个新的类
CtClass ctClass = classPool.makeClass("org.example.haha");
// 创建类成员变量content
CtField ctField = CtField.make("private static String content = \"Hello world~\";", ctClass);
// 将成员变量添加到ctClass对象中
ctClass.addField(ctField);
// 创建一个主方法并输出content对象值
CtMethod ctMethod = CtMethod.make(
"public static void main(String[] args) { System.out.println(content); }", ctClass
);
// 将成员方法添加到ctClass对象中
ctClass.addMethod(ctMethod);
// 使用类CtClass,生成类二进制
byte[] bytes = ctClass.toBytecode();
// 输出二进制数据到控制台
System.out.println(Arrays.toString(bytes));
// 将class二进制内容写入到类文件
File classFilePath = new File(new File(System.getProperty("user.dir"), "maven_text/output/org/example"), "haha.class");
FileUtils.writeByteArrayToFile(classFilePath, bytes);
// 将生成的类写入文件系统
ctClass.writeFile("output");
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
看一看代码理解一下。
然后生成如下目录结构:

haha.class的内容同上。
有个点稍微说明一下,使用如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package org.example;
import javassist.*;
public class Main {
public static void main(String[] args) throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass("org.example.erqi");
CtField ctField = CtField.make("public String name = \"ahhaha\"; ",ctClass);
ctClass.addField(ctField);
CtMethod ctMethod = CtMethod.make("public void setName(String name){ this.name = name ;}",ctClass);
ctClass.addMethod(ctMethod);
Class clazz = ctClass.toClass();
System.out.println(ctClass);
System.out.println(clazz);
}
}
|
输出为:
1
2
3
|
javassist.CtNewClass@67f89fa3[hasConstructor changed frozen public class org.example.erqi fields=org.example.erqi.name:Ljava/lang/String;, constructors=javassist.CtConstructor@4ac68d3e[public erqi ()V], methods=javassist.CtMethod@ab2416c4[public setName (Ljava/lang/String;)V], ]
class org.example.erqi
|
细心的师傅可能都已经发现了,对于前面所有的用过的代码,我们的操作层面都是字节码,所以生成的文件都是class文件,可以理解一下这个输出结果。
最后,既然我们都已经生成了.class
文件,那么我们现在就可以利用很多方法了,比如动态加载字节码。注意思考。
参考文章:
https://cloud.tencent.com/developer/article/1815164
https://blog.csdn.net/google20/article/details/144730353
https://nivi4.notion.site/Java-Javassist-621beee2064a4494abe794843028449d