Javassist 使用指南(三)

本文翻譯自 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.AnnotationsAttributejavassist.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 中。

上一篇:Javassist 使用指南(二)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 8086匯編 本筆記是筆者觀看小甲魚老師(魚C論壇)《零基礎入門學習匯編語言》系列視頻的筆記,在此感謝他和像他一樣...
    Gibbs基閱讀 37,391評論 8 114
  • 本文翻譯自 Javassist Tutorial-2 4. 自省和自定制 (Introspection and c...
    二胡閱讀 32,444評論 4 33
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,766評論 18 399
  • 本周小結 跑步2次,10km。 讀書2本(非技術),讀完1本,還有1本在讀。 本周主要把時間花在技術上重新理清整個...
    im天行閱讀 308評論 0 0
  • 小空同學: 這兩天你把很多注意力用在了訂酒店上,帶著媽媽和孩子旅行要考慮很多,不過花太多精力還是有些浪費...
    小空同學閱讀 114評論 0 0