系列文章目录
第一章 读写字节码
第二章 类池
第三章 类加载器
第四章 类加载器
前言
在上一章我们介绍了Javassist的常见标识符、更改方法体以及新增或删除方法和字段等。本篇文章将介绍Javassist官方文档的最后五个章节的内容。
5.字节码级API
Javassist还提供了用于直接编辑类文件的低级API。要使用这一级别的API,您需要详细了解Java字节码和类文件格式,这一级别的API允许您对类文件进行任何修改。
如果您只想生成一个简单的类文件,javassist.bytecode.ClassFileWriter对您来说是最合适的api。尽管它的API是最小的,它比javassist.bytecode.ClassFile的api还要快得多。
5.1获取ClassFile对象
javassist.bytecode.ClassFile对象表示类文件。要获取此对象,应调用CtClass中的getClassFile方法。
否则,您可以直接从类文件构造一个javassist.bytecode.ClassFile。例如
BufferedInputStream fin
= new BufferedInputStream(new FileInputStream("Point.class"));
ClassFile cf = new ClassFile(new DataInputStream(fin));
这段代码从Point.class字节码文件创建ClassFile对象。
ClassFile对象可以写回类文件。write方法将类文件的内容写入指定的DataOutputStream。
您可以从头开始创建新的类文件。例如:
ClassFile cf = new ClassFile(false, "test.Foo", null);
cf.setInterfaces(new String[] { "java.lang.Cloneable" });
FieldInfo f = new FieldInfo(cf.getConstPool(), "width", "I");
f.setAccessFlags(AccessFlag.PUBLIC);
cf.addField(f);
cf.write(new DataOutputStream(new FileOutputStream("Foo.class")));
该代码生成一个类文件Foo。类,该类包含以下类的实现:
package test;
class Foo implements Cloneable {
public int width;
}
5.2 添加和删除成员
ClassFile提供addField方法和addMethod方法来添加字段或方法(注意,构造函数被在字节码级别中被视为方法)。它还提供了addAttribute方法,用于向类文件添加属性。
请注意,FieldInfo、MethodInfo和AttributeInfo对象包含指向ConstPool(常量池表)对象的链接。ConstPool对象必须与ClassFile对象和添加到该ClassFile对象的FieldInfo(或MethodInfo等)对象共用。换句话说,FieldInfo(或MethodInfo等)对象不能在不同的ClassFile对象之间共享。
要从ClassFile对象中删除字段或方法,必须首先获得包含类的所有字段的java.util.List 对象。getFields方法和getMethods方法返回这个列表对象。可以通过对列表对象调用remove方法来删除字段或方法。属性也可以用类似的方式删除。在FieldInfo或MethodInfo中调用getAttributes方来获取属性列表,并从列表中删除一个。
5.3遍历方法体
要检查方法体中的每个字节码指令,CodeIterator非常有用。要获取此对象,请执行以下操作:
ClassFile cf = ... ;
MethodInfo minfo = cf.getMethod("move"); // 我们假设move方法没有重载。
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator i = ca.iterator();
CodeIterator对象允许您从头到尾逐个访问每个字节码指令。以下是CodeIterator中声明的部分方法:
- void begin() 移至第一条指令。
- void move(int index) 移动到指定索引的指令。
- boolean hasNext() 如果有更多指令,则返回true。
- int next() 返回下一条指令的索引。 注意,它不会返回下一条指令的操作码。
- int byteAt(int index) 返回索引处的无符号8位值。
- int u16bitAt(int index) 返回索引处的无符号16位值。
- int write(byte[] code, int index) 在索引处写入字节数组。
- void insert(int index, byte[] code) 在索引处插入字节数组。分支偏移等自动调整。
以下代码段展示了方法体中包含的所有指令:
CodeIterator ci = ... ;
while (ci.hasNext()) {
int index = ci.next();
int op = ci.byteAt(index);
System.out.println(Mnemonic.OPCODE[op]);
}
5.4 生成字节码序列
Bytecode对象表示字节码指令序列。它是一个可增长的字节码数组。以下代码是一个示例:
ConstPool cp = ...; // 常量池表
Bytecode b = new Bytecode(cp, 1, 0);
b.addIconst(3);
b.addReturn(CtClass.intType);
CodeAttribute ca = b.toCodeAttribute();
这将生成表示以下序列的代码属性:
iconst_3
ireturn
您还可以通过调用Bytecode中get方法来获取包含此序列的字节数组。可以将获得的数组插入到另一个代码属性中。
Bytecode不仅提供了许多向序列中添加特定指令的方法,还提供了用于添加8位操作码的addOpcode方法和用于添加索引的addIndex方法。每个操作码的8位值都在opcode接口中定义。
除非控制流不包含分支,否则addOpcode方法和其他用于添加特定指令的方法将自动保持最大堆栈深度。该值可以调用Bytecode对象的getMaxStack方法获得。它也反映在由Bytecode对象构造的CodeAttribute对象上。若要重新计算方法体的最大堆栈深度,请调用CodeAttribute中的computeMaxStack方法。
Bytecode 可用于构造方法。例如
ClassFile cf = ...
Bytecode code = new Bytecode(cf.getConstPool());
code.addAload(0);
code.addInvokespecial("java/lang/Object", MethodInfo.nameInit, "()V");
code.addReturn(null);
code.setMaxLocals(1);
MethodInfo minfo = new MethodInfo(cf.getConstPool(), MethodInfo.nameInit, "()V");
minfo.setCodeAttribute(code.toCodeAttribute());
cf.addMethod(minfo);
此代码生成默认构造函数,并将其添加到cf指定的类中。Bytecode对象首先转换为CodeAttribute对象,然后添加到minfo指定的方法中。该方法最终被添加到类文件cf中。
5.5 注解(元标记)
注解作为运行时不可见(或可见)的属性存储在类文件中。这些属性可以从ClassFile、MethodInfo或FieldInfo对象中调用getAttribute(AnnotationsAttribute.invisibleTag)方法获得。有关更多详细信息,请参阅javassist.bytecode.AnnotationsAttribute类和javassist.bytecode.annotation包。
Javassist还允许您通过高级API访问注解。如果要通过CtClass访问注解,在CtClass或CtBehavior中调用getAnnotations方法即可。
6.泛型
Javassist的低级API完全支持Java5引入的泛型。另一方面,CtClass等高级API不直接支持泛型。然而,这对于字节码转换来说并不是一个严重的问题。
Java的泛型是通过擦除技术实现的。编译后,所有类型参数都会被删除。例如,假设源代码声明了一个参数化类型Vector:
Vector<String> v = new Vector<String>();
:
String s = v.get(0);
编译的字节码相当于以下代码:
Vector v = new Vector();
:
String s = (String)v.get(0);
因此,当您编写字节码转换器时,您可以删除所有类型参数。由于Javassist中嵌入的编译器不支持泛型,因此如果源代码是由Javassist编译的,例如,通过CtMethod.make(),则必须在调用方站点插入显式类型转换。如果源代码由普通Java编译器(如javac)编译,则不需要类型转换。
例如,如果您有一个类:
public class Wrapper<T> {
T value;
public Wrapper(T t) { value = t; }
}
并希望将接口Getter添加到类Wrapper:
public interface Getter<T> {
T get();
}
那么您真正需要添加的接口是Getter(类型参数会丢弃),您需要添加到Wrapper类的方法是这样的一个简单方法:
public Object get() { return value; }
请注意,不需要类型参数。由于get方法返回Object,如果源代码是由Javassist编译的,则需要在调用方进行显式类型转换。例如,如果类型参数T为String,则必须按如下方式插入(String):
Wrapper w = ...
String s = (String)w.get();
如果源代码由普通Java编译器编译,则不需要类型转换,因为编译器会自动插入类型转换。
如果需要在运行时通过反射访问类型参数,则必须向类文件添加泛型签名。有关更多详细信息,请参阅CtClass中setGenericSignature方法的API文档(javadoc)。
7.可变参数
目前,Javassist不直接支持可变参数。因此,要使用varargs创建方法,必须显式设置方法修饰符。但这很容易。假设现在要使用以下方法:
public int length(int... args) { return args.length; }
使用Javassist的以下代码将生成上述方法:
CtClass cc = /* 目标类 */;
CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc);
m.setModifiers(m.getModifiers() | Modifier.VARARGS);
cc.addMethod(m);
参数类型int…更改为int[]和Modifier。VARARGS被添加到方法修饰符中。
要在Javassist中嵌入的编译器编译的源代码中调用此方法,必须编写:
length(new int[] { 1, 2, 3 });
而不是使用varargs机制进行此方法调用:
length(1, 2, 3);
8. J2ME
如果修改J2ME执行环境的类文件,则必须执行预验证。预验证基本上是生成堆栈映射,这类似于JDK1.6中引入J2SE的堆栈映射表。Javassist仅在avassist.bytecode.MethodInfo.doPreverify为真时维护堆栈映射。
还可以为修改的方法手动生成堆栈映射。对于CtMethod对象m表示的给定方法,可以通过调用以下方法生成堆栈映射:
m.getMethodInfo().rebuildStackMapForME(cpool);
这里,cpool是一个ClassPool对象,可以通过调用CtClass对象中的getClassPool方法来获得。ClassPool对象负责从给定的类路径中查找类文件。要获取所有CtMethod对象,调用CtClass对象的getDeclaredMethods方法即可。
9.装箱/拆箱
Java中的装箱和拆箱是语法糖。没有用于装箱或拆箱的字节码。所以Javassist的编译器不支持它们。例如,以下语句在Java中有效:
Integer i = 3;
因为装箱是隐式执行的。但是,对于Javassist,必须将值类型从int显式转换为Integer:
Integer i = new Integer(3);
10. Debug
设置CtClass.debugDump为目录名。然后,Javassist修改和生成的所有类文件都保存在该目录中。要停止此操作,请设置CtClass.debugDump为null。默认值为null。
例如:
CtClass.debugDump = "./dump";
所有被修改的类文件都保存在./dump中。
总结
本篇文章介绍了Javassist的字节码级的api,以及javassist的一些其他特性。至此2022年12月18日,Javassist官网教程已翻译完成。Javassist中文技术资料过于匮乏,所以博主才有了翻译Javassist官方教程的想法。后续会准备一些Javassist在实际项目中使用案例。进一步了解Javassist框架。