本文翻譯自 Javassist Tutorial-3
5. 字節碼操作
Javassist 還提供了用于直接編輯類文件的低級級 API。 使用此 API之前,你需要詳細了解Java 字節碼和類文件格式,因為它允許你對類文件進行任意修改。
如果你只想生成一個簡單的類文件,使用javassist.bytecode.ClassFileWriter
就足夠了。 它比javassist.bytecode.ClassFile
更快而且更小。
獲取 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 對象可以寫回類文件。ClassFile 的 write() 將類文件的內容寫入給定的 DataOutputStream。
5.2 添加和刪除成員
ClassFile 提供了 addField(),addMethod() 和 addAttribute(),來向類添加字段、方法和類文件屬性。
注意,FieldInfo,MethodInfo 和 AttributeInfo 對象包括到 ConstPool(常量池表)對象的鏈接。 ConstPool 對象必須對 ClassFile 對象和添加到該 ClassFile 對象的 FieldInfo(或MethodInfo 等)對象是通用的。 換句話說,FieldInfo(或MethodInfo等)對象不能在不同的ClassFile 對象之間共享。
要從 ClassFile 對象中刪除字段或方法,必須首先獲取包含該類的所有字段的 java.util.List 對象。 getFields() 和 getMethods() 返回列表。可以通過在List對象上調用 remove() 來刪除字段或方法。可以以類似的方式去除屬性。在 FieldInfo 或 MethodInfo 中調用 getAttributes() 以獲取屬性列表,并從列表中刪除一個。
5.3 遍歷方法體
使用 CodeIterator 可以檢查方法體中的每個字節碼指令,要獲得 CodeIterator 對象,參考以下代碼:
ClassFile cf = ... ;
MethodInfo minfo = cf.getMethod("move"); // we assume move is not overloaded.
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator ci = ca.iterator();
CodeIterator 對象允許你逐個訪問每個字節碼指令。下面展示了一部分 CodeIterator 中聲明的方法:
- void begin()
移動到第一條指令。 - void move(int index)
移動到指定位置的指令。 - boolean hasNext()
是否有下一條指定 - 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 = ...; // constant pool table
Bytecode b = new Bytecode(cp, 1, 0);
b.addIconst(3);
b.addReturn(CtClass.intType);
CodeAttribute ca = b.toCodeAttribute();
這段代碼產生以下序列的代碼屬性:
iconst_3
ireturn
您還可以通過調用 Bytecode 中的 get() 方法來獲取包含此序列的字節數組。獲得的數組可以插入另一個代碼屬性。
Bytecode 提供了許多方法來添加特定的指令,例如使用 addOpcode() 添加一個 8 位操作碼,使用 addIndex() 用于添加一個索引。每個操作碼的值定義在 Opcode 接口中。
addOpcode() 和添加特定指令的方法,將自動維持最大堆棧深度,除非控制流沒有分支。可以通過調用 Bytecode 的 getMaxStack() 方法來獲得這個深度。它也反映在從 Bytecode對象構造的 CodeAttribute 對象上。要重新計算方法體的最大堆棧深度,可以調用 CodeAttribute 的 computeMaxStack() 方法。
5.5 注釋(元標簽)
注釋作為運行時不可見(或可見)的注記屬性,存儲在類文件中。調用 getAttribute(AnnotationsAttribute.invisibleTag)方法,可以從 ClassFile,MethodInfo 或 FieldInfo 中獲取注記屬性。更多信息,請參閱 javassist.bytecode.AnnotationsAttribute
和javassist.bytecode.annotation
包的 javadoc 手冊。
Javassist還允許您通過更高級別的API訪問注釋。 如果要通過CtClass訪問注釋,請在CtClass或CtBehavior中調用getAnnotations()。
6. 泛型
Javassist 的低級別 API 完全支持 Java 5 引入的泛型。但是,高級別的API(如CtClass)不直接支持泛型。
Java 的泛型是通過擦除技術實現。 編譯后,所有類型參數都將被刪除。 例如,假設您的源代碼聲明一個參數化類型 Vector<String>:
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<T> 到類 Wrapper<T>:
public interface Getter<T> {
T get();
}
那么你真正要添加的接口其實是Getter(將類型參數<T>掉落),最后你添加到 Wrapper 類的方法是這樣的:
public Object get() { return value; }
注意,不需要類型參數。 由于 get 返回一個 Object,如果源代碼是由 Javassist 編譯的,那么在調用方需要進行顯式類型轉換。 例如,如果類型參數 T 是 String,則必須插入(String),如下所示:
Wrapper w = ...
String s = (String)w.get();
7.可變參數
目前,Javassist 不直接支持可變參數。 因此,要使用 varargs 創建方法,必須顯式設置方法修飾符。假設要定義下面這個方法:
public int length(int... args) { return args.length; }
使用 Javassist 應該是這樣的:
CtClass cc = /* target class */;
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 });
而不是這樣:
length(1, 2, 3);
8. J2ME
如果要修改 J2ME 執行環境的類文件,則必須先執行預驗證。預驗證基本上是生成堆棧映射,這類似于在 JDK 1.6 中引入 J2SE 的堆棧映射表。當javassist.bytecode.MethodInfo.doPreverify
為 true 時,Javassist 才會維護 J2ME 的堆棧映射。
對于指定的 CtMethod 對象,你可以調用以下方法,手動生成堆棧映射:
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. 調試
將 CtClass.debugDump 設為本地目錄。 然后 Javassist 修改和生成的所有類文件都保存在該目錄中。要停止此操作,將 CtClass.debugDump 設置為 null 即可。其默認值為 null。
例如,
CtClass.debugDump =“./dump”;
所有修改的類文件都保存在 ./dump 中。