在本文中,我们将研究Javasisst(http://www.javassist.org/)库。
简单地说,这个库通过使用高级API比JDK中的API更简单地操作Java字节码。
Maven依赖
要将Javassist库添加到我们的项目中,我们需要将Javassist添加到pom中:
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>${javaassist.version}</version>
</dependency>
<properties>
<javaassist.version>3.21.0-GA</javaassist.version>
</properties>
字节码是什么?
在非常高的层次上,每个Java类都是以纯文本格式编写并编译成字节码的,字节码是Java虚拟机可以处理的指令集。JVM将字节码指令转换为机器级汇编指令。
假设我们有一个Point
类:
public class Point {
private int x;
private int y;
public void move(int x, int y) {
this.x = x;
this.y = y;
}
// standard constructors/getters/setters
}
编译后,将创建包含字节码的Point.class
文件。通过执行javap
命令,我们可以看到该类的字节码:
javap -c Point.class
这将打印以下输出:
public class com.baeldung.javasisst.Point {
public com.baeldung.javasisst.Point(int, int);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field x:I
9: aload_0
10: iload_2
11: putfield #3 // Field y:I
14: return
public void move(int, int);
Code:
0: aload_0
1: iload_1
2: putfield #2 // Field x:I
5: aload_0
6: iload_2
7: putfield #3 // Field y:I
10: return
}
所有这些指令都由Java语言指定;它们中有很多是可用的。
让我们分析move()
方法的字节码指令:
aload_0
指令正在从局部变量0向堆栈加载引用iload_1
正在从局部变量1加载一个int值putfield
正在设置对象的字段x
。对于y
,所有操作都是类似的- 最后一条指令是返回
每一行Java代码都被编译成字节码,并带有适当的指令。Javassist库使得操作字节码相对容易。
生成Java类
Javassist库可用于生成新的Java类文件。
假设我们想要生成一个实现java.lang.Cloneable
接口的JavassistGeneratedClass
类。我们希望该类具有int类型的id字段。类文件用于创建新类文件,FieldInfo
用于向类添加新字段:
ClassFile cf = new ClassFile(
false, "com.baeldung.JavassistGeneratedClass", null);
cf.setInterfaces(new String[] {"java.lang.Cloneable"});
FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);
创建JavassistGeneratedClass.class
后,我们可以断言它实际上有一个id字段:
ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
assertEquals(fields[0].getName(), "id");
加载类的字节码指令
如果我们想加载一个已经存在的类方法的字节码指令,我们可以获得该类的特定方法的CodeAttribute
。然后我们可以得到一个CodeIterator
来迭代该方法的所有字节码指令。
让我们加载Point
类的move()
方法的所有字节码指令:
ClassPool cp = ClassPool.getDefault();
ClassFile cf = cp.get("com.baeldung.javasisst.Point")
.getClassFile();
MethodInfo minfo = cf.getMethod("move");
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();
List<String> operations = new LinkedList<>();
while (ci.hasNext()) {
int index = ci.next();
int op = ci.byteAt(index);
operations.add(Mnemonic.OPCODE[op]);
}
assertEquals(operations,
Arrays.asList(
"aload_0",
"iload_1",
"putfield",
"aload_0",
"iload_2",
"putfield",
"return"));
通过将字节码聚合到操作列表中,我们可以看到move()
方法的所有字节码指令,如上面的断言所示。
向现有类字节码添加字段
假设我们想在现有类的字节码中添加一个int类型的字段。我们可以使用ClassPoll
加载该类,并在其中添加一个字段:
ClassFile cf = ClassPool.getDefault()
.get("com.baeldung.javasisst.Point").getClassFile();
FieldInfo f = new FieldInfo(cf.getConstPool(), "id", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);
我们可以使用反射来验证Point
类上是否存在id字段:
ClassPool classPool = ClassPool.getDefault();
Field[] fields = classPool.makeClass(cf).toClass().getFields();
List<String> fieldsList = Stream.of(fields)
.map(Field::getName)
.collect(Collectors.toList());
assertTrue(fieldsList.contains("id"));
向类字节码添加构造函数
我们可以使用addInvokespecial()
方法将构造函数添加到前面一个示例中提到的现有类中。
我们可以通过调用java.lang.Object
类中的<init>
方法来添加无参数构造函数:
ClassFile cf = ClassPool.getDefault()
.get("com.baeldung.javasisst.Point").getClassFile();
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);
MethodInfo minfo = new MethodInfo(
cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);
我们可以通过迭代字节码来检查是否存在新创建的构造函数:
CodeIterator ci = code.toCodeAttribute().iterator();
List<String> operations = new LinkedList<>();
while (ci.hasNext()) {
int index = ci.next();
int op = ci.byteAt(index);
operations.add(Mnemonic.OPCODE[op]);
}
assertEquals(operations,
Arrays.asList("aload_0", "invokespecial", "return"));
结论
在本文中,我们介绍了Javassist库,目的是简化字节码操作。
我们关注核心特性,并从Java代码生成一个类文件;我们还对已经创建的Java类进行了字节码操作。