目录
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
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文件
实现步骤:
- 创建一个新类,类名为UserEntity
- 给新类创建一个无参构造函数
- 添加一个私有属性,name,并初始化属性值为“张三”
- 创建一个有参构造函数,构造函数方法为name
- 实现属性name的getter和setter方法
- 添加自定义方法sayHello
- 生成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内容:
控制台输出:
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插入的方法。
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一直不生效:
其实不是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的值已被修改:
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 的动态进入加载机制,保证修改应用立即生效。