目录

1、前言

2、什么是 Javassist?

2.1、Javassist 的特点

2.2、应用场景

3、快速开始

3.1、maven依赖

3.2、生成一个class文件

3.2.1、具体代码

3.2.2、执行结果

3.3、 修改已有类的方法

3.3.1、具体代码

3.3.2、执行结果

3.3.3、问题踩坑

3.4、修改属性值

3.4.1、具体代码

3.4.2、执行结果

4、原理机制


1、前言


在 Java Agent 开发中,动态字节码增强是一项核心技术,而 Javassist 是一个高效且易用的字节码操作库。相比其他工具(如 ASM),Javassist 的语法更加接近 Java 源码,降低了学习成本,非常适合作为初学者的入门工具。


2、什么是 Javassist?


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


来自于: Javassist by jboss-javassist




Java Agent(二)、Javassist入门_开发语言


2.1、Javassist 的特点


  • 高层水平 API:允许直接使用描述类和方法的 Java 语句來修改代码,无需下水操作字节码。
  • 超级的可视化:对类和方法进行叠肥操作时,如同操作源代码。
  • 兼容性强:支持 Java 版本并与字节码的直接作用。


2.2、应用场景


通常被使用在以下几个场景:


  • 动态代理
  • 性能监控
  • 日志增强
  • 方法拦截


3、快速开始


废话不多说,我们直接手撸一套代码后,再根据代码来理解。目前Javassist最新版本已经发布到3.30.2-GA,我们直接使用最新版本来写demo。


3.1、maven依赖


首先添加javassist的maven依赖,当前最新版本为3.30.2-GA版本:


<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.30.2-GA</version>
</dependency>


3.2、生成一个class文件


实现步骤:


  1. 创建一个新类,类名为UserEntity
  2. 给新类创建一个无参构造函数
  3. 添加一个私有属性,name,并初始化属性值为“张三”
  4. 创建一个有参构造函数,构造函数方法为name
  5. 实现属性name的getter和setter方法
  6. 添加自定义方法sayHello
  7. 生成class文件,并调用方法。且能正常执行


期望生成的UserEntity.class内容为:


public class UserEntity {
    private String name = "张三";

    public UserEntity() {
    }

    public String getName() {
        return this.name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public UserEntity(String name) {
        this.name = name;
    }

    public String sayHello() {
        return "Hello, Javassist。I am :" + this.name;
    }
}


3.2.1、具体代码


package org.example.javassist;

import javassist.*;

public class JavassistDemo {

    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        // 创建一个新类
        CtClass userClass = classPool.makeClass("org.example.javassist.loader.UserEntity");

        // 添加无参构造
        CtConstructor noArgsConstructor = new CtConstructor(new CtClass[]{}, userClass);
        noArgsConstructor.setBody("{}");
        userClass.addConstructor(noArgsConstructor);

        // 新增字段name
        CtField nameField = new CtField(classPool.get("java.lang.String"), "name", userClass);
        nameField.setModifiers(Modifier.PRIVATE);
        userClass.addField(nameField, CtField.Initializer.constant("张三"));

        // getter/setter
        userClass.addMethod(CtNewMethod.getter("getName", nameField));
        userClass.addMethod(CtNewMethod.setter("setName", nameField));

        // 添加有参构造
        CtConstructor argsConstructor = new CtConstructor(new CtClass[]{
                classPool.get("java.lang.String")
        }, userClass);
        // $0表示this,$1表示参数
        argsConstructor.setBody("{$0.name = $1;}");
        userClass.addConstructor(argsConstructor);

        // 添加自定义方法 1
        CtMethod sayHelloMethod = CtNewMethod.make(
                "public String sayHello() { return \"Hello, Javassist。I am :\" + name; }",
                userClass
        );
        userClass.addMethod(sayHelloMethod);

        // 添加自定义方法 2
        CtMethod sayHiMethod = new CtMethod(classPool.get("java.lang.String"), "sayHi", new CtClass[]{}, userClass);
        sayHiMethod.setBody("{return \"Hi, Javassist。I am :\" + name;}");
        sayHiMethod.setModifiers(Modifier.PUBLIC);
        userClass.addMethod(sayHiMethod);

        // 生成类
        userClass.writeFile("E:\\idea_projects\\java-agent-demo\\src\\main\\java\\");
        Class<?> clazz = userClass.toClass();
        Object instance = clazz.getDeclaredConstructor().newInstance();
        System.out.println(clazz.getMethod("sayHello").invoke(instance));
        System.out.println(clazz.getMethod("sayHi").invoke(instance));

    }

}


3.2.2、执行结果


执行后,会在本地生成一份UserEntity.class文件,并调用了自定义方法,打印了相应内容。


UserEntity.class内容:



Java Agent(二)、Javassist入门_Java_02


控制台输出:



Java Agent(二)、Javassist入门_开发语言_03


3.3、 修改已有类的方法


已有CatEntity.java类,通过javassist方法在sayHello方法前后,追加两个方法。这类场景一般用于日志切面,权限切面比较多。


3.3.1、具体代码


CatEntity.java:


public class CatEntity {

    // 注意这里是void,不是返回String
    public void sayHello(){
        System.out.println("cat say hello:miao~");
    }
}


Javassist对他进行修改:


package org.example.javassist;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

public class JavassistModifyMethodDemo {

    public static void main(String[] args) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        // 获取某个要修改的类
        CtClass catClass = classPool.get("org.example.javassist.loader.CatEntity");

        // 获取sayHello方法
        CtMethod ctMethod = catClass.getDeclaredMethod("sayHello");

        // 在方法前添加代码
        ctMethod.insertBefore("System.out.println(\"cat say hello begin...\");");

        // 在方法后添加代码
        ctMethod.insertAfter("System.out.println(\"cat say hello finish!!!\");");

        // 重新定义类
        Class<?> clazz = catClass.toClass();
        Object instance = clazz.getDeclaredConstructor().newInstance();
        clazz.getMethod("sayHello").invoke(instance);
    }
}


3.3.2、执行结果


最终打印结果,可以发现在sayHello前后分别调用了insertBefore插入的方法和insertAfter插入的方法。



Java Agent(二)、Javassist入门_java_04


3.3.3、问题踩坑


关于CatEntity.java的sayHello方法是void类型。我在实验的时候,刚开始的是:


public String sayHello(){
        return "cat say hello:miao~";
    }


在Javassist中修改方式为:


public static void main(String[] args) throws Exception {
    ClassPool classPool = ClassPool.getDefault();
    // 获取某个要修改的类
    CtClass catClass = classPool.get("org.example.javassist.loader.CatEntity");

    // 获取sayHello方法
    CtMethod ctMethod = catClass.getDeclaredMethod("sayHello");

    // 在方法前添加代码
    ctMethod.insertBefore("System.out.println(\"cat say hello begin...\");");

    // 在方法后添加代码
    ctMethod.insertAfter("System.out.println(\"cat say hello finish!!!\");");

    // 重新定义类
    Class<?> clazz = catClass.toClass();
    Object instance = clazz.getDeclaredConstructor().newInstance();
    System.out.println(clazz.getMethod("sayHello").invoke(instance));
}


注意看最后一行代码,这里直接打印出来。最后发现insertAfter一直不生效:



Java Agent(二)、Javassist入门_字节码_05


其实不是insertAfter不生效,而是因为sayHello的返回值return了,而在最后一行代码执行时return后才打印了返回值,因此是合理的。


3.4、修改属性值


前面调用的都是原本类提供的方法,这里修改某个类的属性值。


3.4.1、具体代码


public static void main(String[] args) throws Exception {
    ClassPool classPool = ClassPool.getDefault();
    // 创建一个新类
    CtClass userClass = classPool.makeClass("org.example.javassist.loader.UserEntity");

    ... 忽略 ...

    // 生成类
    userClass.writeFile("E:\\idea_projects\\java-agent-demo\\src\\main\\java\\");
    Class<?> clazz = userClass.toClass();
    Object instance = clazz.getDeclaredConstructor().newInstance();
    System.out.println(clazz.getMethod("sayHello").invoke(instance));
    System.out.println(clazz.getMethod("sayHi").invoke(instance));

    // 调用新的方法,进行赋值
    clazz.getMethod("setName", String.class).invoke(instance, "李四");
    System.out.println(clazz.getMethod("sayHello").invoke(instance));
    System.out.println(clazz.getMethod("sayHi").invoke(instance));

}


3.4.2、执行结果


打印结果发现,属性name的值已被修改:



Java Agent(二)、Javassist入门_字节码_06


4、原理机制


通过前面的示例代码可以看到,通过Javassist提供的一些api可以很顺利的对字节码进行操作,按需实现部分动态的修改。这里简单介绍下其实现的原理机制。


  • ClassPool 资源管理


ClassPool 是 Javassist 中用于管理和加载类的核心构件。在运行时,它会加载指定类进入内存,以 CtClass 对象的形式为基础,做出修改。修改完成后,再将 CtClass 生成 Java Class 对象。


  • CtClass 和字节码操作


  • CtClass 代表一个可修改的 Java 类,它提供了很多方法,如修改类名,添加字段和方法,以及接口。
  • 字节码操作举例:在方法前后添加自定义代码,改变类的主体行为,而无需修改源代码。
  • CtClass几个常用方法:
  • freeze : 冻结一个类,使其不可修改;
  • isFrozen : 判断一个类是否已被冻结;
  • prune : 删除类不必要的属性,以减少内存占用。调用该方法后,许多方法无法将无法正常使用,慎用;
  • defrost : 解冻一个类,使其可以被修改。如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
  • detach : 将该class从ClassPool中删除;
  • writeFile : 根据CtClass生成 .class 文件;
  • toClass : 通过类加载器加载该CtClass


  • 字节码生成和封装


Javassist 对 Java 类和字节码进行了封装,通过清晰的 API 接口,实现字节码的动态生成和操作。这使得字节码操作更加容易,可以在不改变源码的情况下,实现需要的功能增强。


  • 生成 Class 和加载过程


修改完成的 CtClass 对象,通过 toClass 方法生成最终的 Java Class,这个过程基于 JVM 的动态进入加载机制,保证修改应用立即生效。