Javassist

Java学习

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,

  • setModifiers:可修改类的访问修饰符,

  • insertBeforeinsertAfter:能够实现在类方法执行的前后插入任意的Java代码片段,

  • setBody :可以修改整个方法的代码等。

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

![image-20240622210215625](Java Javassist/image-20240622210215625.png)

内容为:

 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();
        }
    }
}

看一看代码理解一下。

然后生成如下目录结构:

![image-20240622211651698](Java Javassist/image-20240622211651698.png)

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

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计