Javassist 使用指南(二)

本文翻譯自 Javassist Tutorial-2

4. 自省和自定制 (Introspection and customization)

CtClass 提供了自省的方法。Javassist 的自省能力與 Java 反射 API 兼容。 CtClass 提供了 getName(),getSuperclass(),getMethods() 等方法來(lái)獲取類的信息,也提供了修改類定義的方法(添加字段,添加構(gòu)造函數(shù)、添加方法),同時(shí)也可以對(duì)方法體的語(yǔ)句進(jìn)行檢測(cè)。

方法由 CtMethod 對(duì)象表示。CtMethod 提供了幾個(gè)函數(shù)來(lái)修改方法的定義。
注意,如果一個(gè)方法繼承自一個(gè)超類,那么表示繼承方法的 CtMethod 對(duì)象,同樣也表示該超類中聲明的方法。

例如,如果類 Point 聲明方法 move() , Point 的子類 ColorPoint 不覆蓋 move() ,那么在 Point 中聲明的 move 和 ColorPoint 的 move 具有相同的 CtMethod。 如果由這個(gè) CtMethod 對(duì)象表示的方法定義被修改,那么修改將反映在這兩種方法上。 如果你只想修改 ColorPoint 中的 move() 方法,你首先必須在 Point 中加入 CtMethod 對(duì)象的副本 move() 。CtMethod 對(duì)象的副本可以通過(guò) CtNewMethod.copy() 獲得。

Javassist 不允許刪除方法或字段,但它允許更改名稱。所以,如果一個(gè)方法是沒(méi)有必要的,可以通過(guò)調(diào)用 CtMethod 的 setName() 和 setModifiers() 中將其改為一個(gè)私有方法。

Javassist 不允許向現(xiàn)有方法添加額外的參數(shù)。你可以通過(guò)新建一個(gè)方法達(dá)到同樣的效果。 例如,如果你想為一個(gè)方法添加一個(gè)額外的 int 參數(shù) newZ 到 Point 類,

void move(int newX, int newY) { x = newX; y = newY; }

你可以添加一個(gè)這樣的方法到 Point 類:

void move(int newX, int newY, int newZ) {
    // do what you want with newZ.
    move(newX, newY);
}

Javassist 還提供了用于直接編輯原始類文件的低級(jí)API。
例如,CtClass 中的 getClassFile() 返回一個(gè)表示類文件的 ClassFile 對(duì)象。CtMethod 的 getMethodInfo() 方法返回一個(gè) MethodInfo 對(duì)象,表示類文件中的 method_info 結(jié)構(gòu)。 低級(jí)API使用 Java 虛擬機(jī)規(guī)范中的詞匯表。 用戶必須具有類文件和字節(jié)碼的知識(shí)。有關(guān)更多詳細(xì)信息,請(qǐng)參考 javassist.bytecode 包。

由 Javassist 修改的類文件只有在使用以 $ 開頭的特殊標(biāo)識(shí)符時(shí)才需要 javassist.runtime 包來(lái)提供運(yùn)行時(shí)支持。接下來(lái)的內(nèi)容會(huì)討論這些特殊標(biāo)識(shí)符。在沒(méi)有這些特殊標(biāo)識(shí)符的情況下,在運(yùn)行時(shí)修改類文件不需要 javassist.runtime 包或任何其他 Javassist 包。有關(guān)更多詳細(xì)信息,請(qǐng)參閱 javassist.runtime 包的API文檔。

4.1 在方法體的開始/結(jié)尾處添加代碼

CtMethod 和 CtConstructor 提供了 insertBefore(),insertAfter() 和 addCatch() 方法。 它們可以將用 Java 編寫的代碼片段插入到現(xiàn)有方法中。Javassist 包括一個(gè)用于處理源代碼的簡(jiǎn)單編譯器,它接收用 Java 編寫的源代碼,并將其編譯成 Java 字節(jié)碼,并內(nèi)聯(lián)方法體中。

也可以按行號(hào)來(lái)插入代碼段(如果行號(hào)表包含在類文件中)。向 CtMethod 和 CtConstructor 中的 insertAt() 方法提供源代碼和原始類定義中的源文件的行號(hào),就可以將編譯后的代碼插入到指定行號(hào)位置。

方法 insertBefore() ,insertAfter(),addCatch() 和 insertAt() 接收一個(gè)表示語(yǔ)句或語(yǔ)句塊的 String 對(duì)象。一個(gè)語(yǔ)句是一個(gè)單一的控制結(jié)構(gòu),比如 if 和 while 或者以分號(hào)結(jié)尾的表達(dá)式。語(yǔ)句塊是一組用大括號(hào) {} 包圍的語(yǔ)句。因此,以下每行都是有效語(yǔ)句或塊的示例:

System.out.println("Hello");
{ System.out.println("Hello"); }
if (i < 0) { i = -i; }

語(yǔ)句和語(yǔ)句塊可以引用字段和方法。如果使用 -g 選項(xiàng)(在類文件中包含局部變量屬性)編譯該方法,則它們還可以引用方法的參數(shù)。 否則,它們必須通過(guò)特殊變量 0,1,$2,... 來(lái)訪問(wèn)方法參數(shù),下面會(huì)討論。不允許訪問(wèn)在方法中聲明的局部變量,盡管在塊中聲明一個(gè)新的局部變量是允許的。但是,insertAt() 允許語(yǔ)句和塊訪問(wèn)局部變量,前提是這些變量在指定的行號(hào)處可用,并且目標(biāo)方法是使用 -g 選項(xiàng)編譯的。

傳遞給方法 insertBefore() ,insertAfter() ,addCatch() 和 insertAt() 的 String 對(duì)象是由Javassist 的編譯器編譯的。 由于編譯器支持語(yǔ)言擴(kuò)展,以 $ 開頭的幾個(gè)標(biāo)識(shí)符有特殊的含義:

符號(hào) 含義
$0, $1, $2, ... this and 方法的參數(shù)
$args 方法參數(shù)數(shù)組.它的類型為 Object[]
$$ 所有實(shí)參。例如, m($$) 等價(jià)于 m($1,$2,...)
$cflow(...) cflow 變量
$r 返回結(jié)果的類型,用于強(qiáng)制類型轉(zhuǎn)換
$w 包裝器類型,用于強(qiáng)制類型轉(zhuǎn)換
$_ 返回值
$sig 類型為 java.lang.Class 的參數(shù)類型數(shù)組
$type 一個(gè) java.lang.Class 對(duì)象,表示返回值類型
$class 一個(gè) java.lang.Class 對(duì)象,表示當(dāng)前正在修改的類

0,1, $2, ...

傳遞給目標(biāo)方法的參數(shù)使用 1,2,... 訪問(wèn),而不是原始的參數(shù)名稱。 1 表示第一個(gè)參數(shù),2 表示第二個(gè)參數(shù),以此類推。 這些變量的類型與參數(shù)類型相同。 0 等價(jià)于 `this` 指針。 如果方法是靜態(tài)的,則0 不可用。

下面有一些使用這些特殊變量的例子。假設(shè)一個(gè)類 Point:

class Point {
    int x, y;
    void move(int dx, int dy) { x += dx; y += dy; }
}

要在調(diào)用方法 move() 時(shí)打印 dx 和 dy 的值,請(qǐng)執(zhí)行以下程序:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtMethod m = cc.getDeclaredMethod("move");
m.insertBefore("{ System.out.println($1); System.out.println($2); }");
cc.writeFile();

請(qǐng)注意,傳遞給 insertBefore() 的源文本是用大括號(hào) {} 括起來(lái)的。insertBefore() 只接受單個(gè)語(yǔ)句或用大括號(hào)括起來(lái)的語(yǔ)句塊。

修改后的類 Point 的定義是這樣的:

class Point {
    int x, y;
    void move(int dx, int dy) {
        { System.out.println(dx); System.out.println(dy); }
        x += dx; y += dy;
    }
}

$1 and $2 are replaced with dx and dy, respectively.
$1, $2, $3 ... are updatable. If a new value is assigend to one of those variables, then the value of the parameter represented by that variable is also updated.

1 和2 分別替換為 dx 和 dy。
1,2,$3 ...是可更新的。如果這些變量被賦予新值,則由該變量表示的參數(shù)的值也將被更新。

$args

變量 args 表示所有參數(shù)的數(shù)組。該變量的類型是 Object 類的數(shù)組。如果參數(shù)類型是原始類型(如 int),則該參數(shù)值將轉(zhuǎn)換為包裝器對(duì)象(如java.lang.Integer)以存儲(chǔ)在args 中。 因此,如果第一個(gè)參數(shù)的類型是不是原始類型,那么 args[0] 等于1。注意 args[0] 不等于0,因?yàn)?$0 表示 this。

如果 Object 的數(shù)組被分配給 $args,那么該數(shù)組的每個(gè)元素都被分配給每個(gè)參數(shù)。如果參數(shù)類型是基本類型,則相應(yīng)元素的類型必須是包裝類型。 在將值分配給參數(shù)之前,必須將該值從包裝器類型轉(zhuǎn)換為基本類型。

$$

變量 $$ 是所有參數(shù)列表的縮寫,用逗號(hào)分隔。 例如,如果方法 move() 的有 3 個(gè)參數(shù),則

move($1, $2, $3)

如果 move() 不帶任何參數(shù),則 move()等同于move()。 可以與其他方法一起使用。 如果你寫一個(gè)表達(dá)式:

exMove($$, context)

這個(gè)表達(dá)式等價(jià)于

exMove($1, $2, $3, context)

注意,$$ 開啟了通用符號(hào)方法調(diào)用,它通常與稍后要介紹的 $proceed 一起使用。

$cflow

$ cflow表示 控制流。此只讀變量返回特定方法的遞歸調(diào)用的深度。

假設(shè)下面所示的方法由CtMethod對(duì)象cm表示:

int fact(int n) {
    if (n <= 1)
        return n;
    else
        return n * fact(n - 1);
}

要使用 cflow,首先聲明使用cflow 監(jiān)視方法 fact() 的調(diào)用:

CtMethod cm = ...;
cm.useCflow("fact");

useCflow() 的參數(shù)是 $cflow 變量的標(biāo)識(shí)符。任何有效的 Java 名稱都可以用作標(biāo)識(shí)符。標(biāo)識(shí)符還可以包括 . ,例如,“my.Test.fact”是有效的標(biāo)識(shí)符。

然后,cflow(fact) 表示由 cm 指定的方法的遞歸調(diào)用深度。cflow(fact) 的值在方法第一次調(diào)用時(shí)為 0,而當(dāng)方法在方法中遞歸調(diào)用時(shí)為 1。 例如,

cm.insertBefore("if ($cflow(fact) == 0)"
              + "    System.out.println(\"fact \" + $1);");

翻譯方法fact(),以便它顯示參數(shù)。因?yàn)闄z查了 $cflow(fact) 的值,所以如果在 fact() 中遞歸調(diào)用,則方法 fact() 不會(huì)顯示參數(shù)。

cflow 的值是當(dāng)前線程的最頂層堆棧幀下與 cm 相關(guān)聯(lián)的堆棧幀數(shù)。cflow 也可以不在 cm 方法中訪問(wèn)。

$r

$r 表示方法的結(jié)果類型(返回類型)。它用在 cast 表達(dá)式中作 cast 轉(zhuǎn)換類型。 下面是一個(gè)典型的用法:

Object result = ... ;
$_ = ($r)result;

如果結(jié)果類型是原始類型,則 (r) 遵循特殊語(yǔ)義。 首先,如果 cast 表達(dá)式的操作數(shù)是原始類型,(r) 作為普通轉(zhuǎn)換運(yùn)算符。 另一方面,如果操作數(shù)是包裝類型,(r) 將從包裝類型轉(zhuǎn)換為結(jié)果類型。 例如,如果結(jié)果類型是 int,那么 (r) 將從 java.lang.Integer 轉(zhuǎn)換為 int。

如果結(jié)果類型為void,那么 (r) 不轉(zhuǎn)換類型; 它什么也不做。 但是,如果操作數(shù)是對(duì) void 方法的調(diào)用,則 (r) 將導(dǎo)致 null。 例如,如果結(jié)果類型是 void,而 foo() 是一個(gè) void 方法,那么

$_ = ($r)foo();

是一個(gè)正確的表達(dá)式。

cast 運(yùn)算符 ($r) 在 return 語(yǔ)句中也很有用。 即使結(jié)果類型是 void,下面的 return 語(yǔ)句也是有效的:

return ($r)result;

這里,result是局部變量。 因?yàn)橹付?($r),所以結(jié)果值被丟棄。此返回語(yǔ)句被等價(jià)于:

return;

$w

w 表示包裝類型。它用在 cast 表達(dá)式中作 cast 轉(zhuǎn)換類型。(w) 把基本類型轉(zhuǎn)換為包裝類型。 以下代碼是一個(gè)示例:

Integer i = ($w)5;

包裝后的類型取決于 ($w) 后面表達(dá)式的類型。如果表達(dá)式的類型為 double,則包裝器類型為 java.lang.Double。

If the type of the expression following ($w) is not a primitive type, then ($w) does nothing.
如果下面的表達(dá)式 (w) 的類型不是原始類型,那么(w) 什么也不做。

$_

CtMethod 中的 insertAfter() 和 CtConstructor 在方法的末尾插入編譯的代碼。傳遞給insertAfter() 的語(yǔ)句中,不但可以使用特殊符號(hào)如 0,1。也可以使用 _ 來(lái)表示方法的結(jié)果值。 該變量的類型是方法的結(jié)果類型(返回類型)。如果結(jié)果類型為 void,那么_ 的類型為Object,_ 的值為 null。 雖然由 insertAfter() 插入的編譯代碼通常在方法返回之前執(zhí)行,但是當(dāng)方法拋出異常時(shí),它也可以執(zhí)行。要在拋出異常時(shí)執(zhí)行它,insertAfter() 的第二個(gè)參數(shù) asFinally 必須為true。 如果拋出異常,由 insertAfter() 插入的編譯代碼將作為 finally 子句執(zhí)行。_ 的值 0 或 null。在編譯代碼的執(zhí)行終止后,最初拋出的異常被重新拋出給調(diào)用者。注意,$_ 的值不會(huì)被拋給調(diào)用者,它將被丟棄。

$sig

$sig 的值是一個(gè) java.lang.Class 對(duì)象的數(shù)組,表示聲明的形式參數(shù)類型。

$type

$type 的值是一個(gè) java.lang.Class 對(duì)象,表示結(jié)果值的類型。 如果這是一個(gè)構(gòu)造函數(shù),此變量返回 Void.class。

$class

class 的值是一個(gè) java.lang.Class 對(duì)象,表示編輯的方法所在的類。 即表示0 的類型。

addCatch()

addCatch() 插入方法體拋出異常時(shí)執(zhí)行的代碼,控制權(quán)會(huì)返回給調(diào)用者。 在插入的源代碼中,異常用 $e 表示。

例如:

CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);

轉(zhuǎn)換成對(duì)應(yīng)的 java 代碼如下:

try {
    // the original method body
} catch (java.io.IOException e) {
    System.out.println(e);
    throw e;
}

請(qǐng)注意,插入的代碼片段必須以 throw 或 return 語(yǔ)句結(jié)束。

4.2 修改方法體

CtMethod 和 CtConstructor 提供 setBody() 來(lái)替換整個(gè)方法體。他將新的源代碼編譯成 Java 字節(jié)碼,并用它替換原方法體。 如果給定的源文本為 null,則替換后的方法體僅包含返回語(yǔ)句,返回零或空值,除非結(jié)果類型為 void。

在傳遞給 setBody() 的源代碼中,以 $ 開頭的標(biāo)識(shí)符具有特殊含義:

符號(hào) 含義
$0, $1, $2, ... this and 方法的參數(shù)
$args 方法參數(shù)數(shù)組.它的類型為 Object[]
$$ 所有實(shí)參。例如, m($$) 等價(jià)于 m($1,$2,...)
$cflow(...) cflow 變量
$r 返回結(jié)果的類型,用于強(qiáng)制類型轉(zhuǎn)換
$w 包裝器類型,用于強(qiáng)制類型轉(zhuǎn)換
$sig 類型為 java.lang.Class 的參數(shù)類型數(shù)組
$type 一個(gè) java.lang.Class 對(duì)象,表示返回值類型
$class 一個(gè) java.lang.Class 對(duì)象,表示當(dāng)前正在修改的類

注意 $_ 不可用。

替換表達(dá)式

Javassist 只允許修改方法體中包含的表達(dá)式。javassist.expr.ExprEditor 是一個(gè)用于替換方法體中的表達(dá)式的類。用戶可以定義 ExprEditor 的子類來(lái)指定修改表達(dá)式的方式。

要運(yùn)行 ExprEditor 對(duì)象,用戶必須在 CtMethod 或 CtClass 中調(diào)用 instrument()。
例如,

CtMethod cm = ... ;
cm.instrument(
    new ExprEditor() {
        public void edit(MethodCall m) throws CannotCompileException {
            if (m.getClassName().equals("Point")
                          && m.getMethodName().equals("move"))
                m.replace("{ $1 = 0; $_ = $proceed($$); }");
        }
    });

上述代碼,搜索由 cm 表示的方法體,并用使用下面的代碼替換 Point 中的 move()調(diào)用:

{ $1 = 0; $_ = $proceed($$); }

因此 move() 的第一個(gè)參數(shù)總是0。注意,替換的代碼不是一個(gè)表達(dá)式,而是一個(gè)語(yǔ)句或塊。 它不能是或包含 try-catch 語(yǔ)句。

方法 instrument() 搜索方法體。 如果它找到一個(gè)表達(dá)式,如方法調(diào)用、字段訪問(wèn)和對(duì)象創(chuàng)建,那么它調(diào)用給定的 ExprEditor 對(duì)象上的 edit() 方法。 edit() 的參數(shù)表示找到的表達(dá)式。 edit() 可以檢查和替換該表達(dá)式。

調(diào)用 edit() 參數(shù)的 replace() 方法可以將表達(dá)式替換為我們給定的語(yǔ)句。如果給定的語(yǔ)句是空塊,即執(zhí)行replace("{}"),則將表達(dá)式刪除。如果要在表達(dá)式之前或之后插入語(yǔ)句(或塊),則應(yīng)該將類似以下的代碼傳遞給 replace():

{ *before-statements;*
  $_ = $proceed($$);
  *after-statements;* }

無(wú)論表達(dá)式是方法調(diào)用、字段訪問(wèn)還是對(duì)象創(chuàng)建或其他。

如果表達(dá)式是讀操作,第二個(gè)語(yǔ)句應(yīng)該是:

$_ = $proceed();

如果表達(dá)式是寫操作,則第二個(gè)語(yǔ)句應(yīng)該是:

$proceed($$);

如果由 instrument() 搜索的方法是使用 -g 選項(xiàng)(類文件包含一個(gè)局部變量屬性)編譯的,目標(biāo)表達(dá)式中可用的局部變量,也可以傳遞給 replace() 的源代碼中使用。

javassist.expr.MethodCall

MethodCall 表示方法調(diào)用。MethodCall 的 replace() 方法用于替換方法調(diào)用,它接收表示替換語(yǔ)句或塊的源代碼。和 insertBefore() 方法一樣,傳遞給 replace 的源代碼中,以 $ 開頭的標(biāo)識(shí)符具有特殊的含義。

符號(hào) 含義
$0 方法調(diào)用的目標(biāo)對(duì)象。它不等于 this,它代表了調(diào)用者。 如果方法是靜態(tài)的,則 $0 為 null
$1, $2 .. 方法的參數(shù)
$_ 方法調(diào)用的結(jié)果
$r 返回結(jié)果的類型,用于強(qiáng)制類型轉(zhuǎn)換
$class 一個(gè) java.lang.Class 對(duì)象,表示當(dāng)前正在修改的類
$sig 類型為 java.lang.Class 的參數(shù)類型數(shù)組
$type 一個(gè) java.lang.Class 對(duì)象,表示返回值類型
$class 一個(gè) java.lang.Class 對(duì)象,表示當(dāng)前正在修改的類
$proceed 調(diào)用表達(dá)式中方法的名稱

這里的方法調(diào)用意味著由 MethodCall 對(duì)象表示的方法。

其他標(biāo)識(shí)符如 w,args 和 $$ 也可用。

除非方法調(diào)用的返回類型為 void,否則返回值必須在源代碼中賦給 \_,_ 的類型是表達(dá)式的結(jié)果類型。如果結(jié)果類型為 void,那么 \_ 的類型為Object,并且分配給_ 的值將被忽略。

$proceed 不是字符串值,而是特殊的語(yǔ)法。 它后面必須跟一個(gè)由括號(hào)括起來(lái)的參數(shù)列表。

javassist.expr.ConstructorCall

ConstructorCall 表示構(gòu)造函數(shù)調(diào)用,例如包含在構(gòu)造函數(shù)中的 this() 和 super()。ConstructorCall 中的方法 replace() 可以使用語(yǔ)句或代碼塊來(lái)代替構(gòu)造函數(shù)。它接收表示替換語(yǔ)句或塊的源代碼。和 insertBefore() 方法一樣,傳遞給 replace 的源代碼中,以 $ 開頭的標(biāo)識(shí)符具有特殊的含義。

符號(hào) 含義
$0 構(gòu)造調(diào)用的目標(biāo)對(duì)象。它等于 this
$1, $2, ... 構(gòu)造函數(shù)的參數(shù)
$class 一個(gè) java.lang.Class 對(duì)象,表示當(dāng)前正在修改的類
$sig 類型為 java.lang.Class 的參數(shù)類型數(shù)組
$proceed 調(diào)用表達(dá)式中構(gòu)造函數(shù)的名稱

這里的構(gòu)造函數(shù)調(diào)用是由 ConstructorCall 對(duì)象表示的。

其他標(biāo)識(shí)符如 w,args 和 $$ 也可用。

由于任何構(gòu)造函數(shù)必須調(diào)用超類的構(gòu)造函數(shù)或同一類的另一個(gè)構(gòu)造函數(shù),所以替換語(yǔ)句必須包含構(gòu)造函數(shù)調(diào)用,通常是對(duì) $proceed() 的調(diào)用。

$proceed 不是字符串值,而是特殊的語(yǔ)法。 它后面必須跟一個(gè)由括號(hào)括起來(lái)的參數(shù)列表。

javassist.expr.FieldAccess

FieldAccess 對(duì)象表示字段訪問(wèn)。 如果找到對(duì)應(yīng)的字段訪問(wèn)操作,ExprEditor 中的 edit() 方法將接收到一個(gè) FieldAccess 對(duì)象。FieldAccess 中的 replace() 方法接收替源代碼來(lái)替換字段訪問(wèn)。

在源代碼中,以 $ 開頭的標(biāo)識(shí)符具有特殊含義:

符號(hào) 含義
$0 表達(dá)式訪問(wèn)的字段。它不等于 this。this 表示調(diào)用表達(dá)式所在方法的對(duì)象。如果字段是靜態(tài)的,則 $0 為 null
$1 如果表達(dá)式是寫操作,則寫的值將保存在 1 中。否則1 不可用
$_ 如果表達(dá)式是讀操作,則結(jié)果值保存在 1 中,否則將舍棄存儲(chǔ)在_ 中的值
$r 如果表達(dá)式是讀操作,則 r 讀取結(jié)果的類型。 否則r 為 void
$class 一個(gè) java.lang.Class 對(duì)象,表示字段所在的類
$type 一個(gè) java.lang.Class 對(duì)象,表示字段的類型
$proceed 執(zhí)行原始字段訪問(wèn)的虛擬方法的名稱

其他標(biāo)識(shí)符如 w,args 和 $$ 也可用。
如果表達(dá)式是讀操作,則必須在源文本中將值分配給 。的類型是字段的類型。

javassist.expr.NewExpr

NewExpr 表示使用 new 運(yùn)算符(不包括數(shù)組創(chuàng)建)創(chuàng)建對(duì)象的表達(dá)式。 如果發(fā)現(xiàn)創(chuàng)建對(duì)象的操作,NewEditor 中的 edit() 方法將接收到一個(gè) NewExpr 對(duì)象。NewExpr 中的 replace() 方法接收替源代碼來(lái)替換字段訪問(wèn)。

在源文本中,以 $ 開頭的標(biāo)識(shí)符具有特殊含義:

符號(hào) 含義
$0 null
$1 構(gòu)造函數(shù)的參數(shù)
$_ 創(chuàng)建對(duì)象的返回值。一個(gè)新的對(duì)象存儲(chǔ)在 $_ 中
$r 所創(chuàng)建的對(duì)象的類型
$sig 類型為 java.lang.Class 的參數(shù)類型數(shù)組
$type 一個(gè) java.lang.Class 對(duì)象,表示創(chuàng)建的對(duì)象的類型
$proceed 執(zhí)行對(duì)象創(chuàng)建虛擬方法的名稱

其他標(biāo)識(shí)符如 w,args 和 $$ 也可用。

javassist.expr.NewArray

NewArray 表示使用 new 運(yùn)算符創(chuàng)建數(shù)組。如果發(fā)現(xiàn)數(shù)組創(chuàng)建的操作,ExprEditor 中的 edit() 方法一個(gè) NewArray 對(duì)象。NewArray 中的 replace() 方法可以使用源代碼來(lái)替換數(shù)組創(chuàng)建操作。

在源文本中,以$開頭的標(biāo)識(shí)符具有特殊含義:

符號(hào) 含義
$0 null
$1, $1 每一維的大小
$_ 創(chuàng)建數(shù)組的返回值。一個(gè)新的數(shù)組對(duì)象存儲(chǔ)在 $_ 中
$r 所創(chuàng)建的數(shù)組的類型
$type 一個(gè) java.lang.Class 對(duì)象,表示創(chuàng)建的數(shù)組的類型
$proceed 執(zhí)行數(shù)組創(chuàng)建虛擬方法的名稱

其他標(biāo)識(shí)符如 w,args 和 $$ 也可用。
例如,如果按下面的方式創(chuàng)建數(shù)組:

String[][] s = new String[3][4];

那么 1 和2 的值分別是 3 和 4。 $3 不可用。

例如,如果按下面的方式創(chuàng)建數(shù)組:

String[][] s = new String[3][];

那么 1 的值為 3,但2 不可用。

javassist.expr.Instanceof

一個(gè) InstanceOf 對(duì)象表示一個(gè) instanceof 表達(dá)式。 如果找到 instanceof 表達(dá)式,則ExprEditor 中的 edit() 方法接收此對(duì)象。Instanceof 中的 replace() 方法可以使用源代碼來(lái)替換 instanceof 表達(dá)式。

在源文本中,以$開頭的標(biāo)識(shí)符具有特殊含義:

符號(hào) 含義
$0 null
$1 instanceof 運(yùn)算符左側(cè)的值
$_ 表達(dá)式的返回值。類型為 boolean
$r instanceof 運(yùn)算符右側(cè)的值
$type 一個(gè) java.lang.Class 對(duì)象,表示 instanceof 運(yùn)算符右側(cè)的類型
$proceed 執(zhí)行 instanceof 表達(dá)式的虛擬方法的名稱。它需要一個(gè)參數(shù)(類型是 java.lang.Object)。如果參數(shù)類型和 instanceof 表達(dá)式右側(cè)的類型一致,則返回 true。否則返回 false。

其他標(biāo)識(shí)符如 w,args 和 $$ 也可用。

javassist.expr.Cast

Cast 表示 cast 表達(dá)式。如果找到 cast 表達(dá)式,ExprEditor 中的 edit() 方法會(huì)接收到一個(gè) Cast 對(duì)象。 Cast 的 replace() 方法可以接收源代碼來(lái)替換替換 cast 表達(dá)式。

在源文本中,以$開頭的標(biāo)識(shí)符具有特殊含義:

符號(hào) 含義
$0 null
$1 顯示類型轉(zhuǎn)換的目標(biāo)類型(?)
$_ 表達(dá)式的結(jié)果值。$_ 的類型和被括號(hào)括起來(lái)的類型相同(?)
$r 轉(zhuǎn)換之后的類型,即被括號(hào)括起來(lái)的類型(?)
$type 一個(gè) java.lang.Class 對(duì)象,和 $r 的類型相同
$proceed 執(zhí)行類型轉(zhuǎn)換的虛擬方法的名稱。它需要一個(gè)參數(shù)(類型是 java.lang.Object)。并在類型轉(zhuǎn)換完成后返回它

其他標(biāo)識(shí)符如 w,args 和 $$ 也可用。

javassist.expr.Handler

Handler 對(duì)象表示 try-catch 語(yǔ)句的 catch 子句。 如果找到 catch,ExprEditor 中的 edit() 方法會(huì)接收此對(duì)象。 Handler 中的 insertBefore() 方法會(huì)將收到的源代碼插入到 catch 子句的開頭。

在源文本中,以$開頭的標(biāo)識(shí)符具有意義:

符號(hào) 含義
$1 catch 分支獲得的異常對(duì)象
$r catch 分支獲得的異常對(duì)象的類型,用于強(qiáng)制類型轉(zhuǎn)換
$w 包裝類型,用于強(qiáng)制類型轉(zhuǎn)換
$type 一個(gè) java.lang.Class 對(duì)象,表示 catch 捕獲的異常的類型

如果一個(gè)新的異常分配給 $1,它將作為捕獲的異常傳遞給原始的 catch 子句。

4.3 添加新方法和字段

添加新方法

Javassist 可以創(chuàng)建新的方法和構(gòu)造函數(shù)。CtNewMethod 和 CtNewConstructor 提供了幾個(gè)工廠方法來(lái)創(chuàng)建 CtMethod 或 CtConstructor 對(duì)象。make() 方法可以通過(guò)源代碼來(lái)CtMethod 或 CtConstructor 對(duì)象。

例如:

CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int xmove(int dx) { x += dx; }",
                 point);
point.addMethod(m);

上面的代碼向類 Point 添加了一個(gè)公共方法 xmove()。在這個(gè)例子中,x 是類 Point 的一個(gè)int 字段。

傳遞給 make() 和 setBody() 的源文本可以包括以 開頭的標(biāo)識(shí)符 (_ 除外)。 如果目標(biāo)對(duì)象和目標(biāo)方法名也被傳遞給 make() 方法,源文本中也可以包括 $proceed。

例如:

CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int ymove(int dy) { $proceed(0, dy); }",
                 point, "this", "move");

這個(gè)程序創(chuàng)建一個(gè) ymove() 方法,定義如下:

public int ymove(int dy) { this.move(0, dy); }

注意,$proceed 已經(jīng)被替換為 this.move。

Javassist 還提供了另一種添加新方法的方式。 你可以先創(chuàng)建一個(gè)抽象方法,然后給它一個(gè)方法體:

CtClass cc = ... ;
CtMethod m = new CtMethod(CtClass.intType, "move",
                          new CtClass[] { CtClass.intType }, cc);
cc.addMethod(m);
m.setBody("{ x += $1; }");
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);

因?yàn)?Javassist 在類中添加了的方法是抽象的,所以在調(diào)用 setBody() 之后,必須將類顯式地改回非抽象類。

相互遞歸的方法 (Mutual recursive methods)

Javassist 不能這種方法:如果它調(diào)用另一個(gè)方法,而另一個(gè)方法沒(méi)有被添加到一個(gè)類(Javassist可以編譯一個(gè)以遞歸方式調(diào)用的方法)。如果要向類添加相互遞歸方法,需要使用如下的技巧。假設(shè)你想要將方法 m() 和 n() 添加到由 cc 表示的類中:

CtClass cc = ... ;
CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc);
CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc);
cc.addMethod(m);
cc.addMethod(n);
m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
n.setBody("{ return m($1); }");
cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT);

你必須先創(chuàng)建兩個(gè)抽象方法,并將它們添加到類中。然后設(shè)置它們的方法體,即使方法體包括互相遞歸的調(diào)用。 最后,必須將類更改為非抽象類。

添加一個(gè)字段

Javassist 還允許用戶創(chuàng)建一個(gè)新字段。

CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f);

該程序向類 Point 添加一個(gè)名為 z 的字段。
如果必須指定添加字段的初始值,那么上面的程序必須修改為:

CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f, "0");  // initial value is 0

現(xiàn)在,方法 addField() 接收兩個(gè)參數(shù),第二個(gè)參數(shù)表示計(jì)算初始值的表達(dá)式。這個(gè)表達(dá)式可以是任意 Java 表達(dá)式,只要其結(jié)果與字段的類型匹配。 請(qǐng)注意,表達(dá)式不以分號(hào)結(jié)尾。

此外,上述代碼可以重寫為更簡(jiǎn)單代碼:

CtClass point = ClassPool.getDefault().get("Point");
CtField f = CtField.make("public int z = 0;", point);
point.addField(f);

刪除成員

要?jiǎng)h除字段或方法,請(qǐng)?jiān)?CtClass 的 removeField() 或 removeMethod() 方法。 一個(gè)CtConstructor 可以通過(guò) CtClass 的 removeConstructor() 刪除。

4.4 注解 (Annotations)

CtClass,CtMethod,CtField 和 CtConstructor 提供 getAnnotations() 方法,用于讀取注解。 它返回一個(gè)注解類型的對(duì)象。

例如,假設(shè)有以下注解:

public @interface Author {
    String name();
    int year();
}

下面是使用注解的代碼:

@Author(name="Chiba", year=2005)
public class Point {
    int x, y;
}

然后,可以使用 getAnnotations() 獲取注解的值。 它返回一個(gè)包含注解類型對(duì)象的數(shù)組。

CtClass cc = ClassPool.getDefault().get("Point");
Object[] all = cc.getAnnotations();
Author a = (Author)all[0];
String name = a.name();
int year = a.year();
System.out.println("name: " + name + ", year: " + year);

這段代碼輸出:

name: Chiba, year: 2005

由于 Point 的注解只有 @Author,所以數(shù)組的長(zhǎng)度是 1,all[0] 是一個(gè) Author 對(duì)象。 注解成員值可以通過(guò)調(diào)用Author對(duì)象的 name() 和 year() 來(lái)獲取。

要使用 getAnnotations(),注釋類型(如 Author)必須包含在當(dāng)前類路徑中。它們也必須也可以從 ClassPool 對(duì)象訪問(wèn)。如果未找到注釋類型的類文件,Javassist 將無(wú)法獲取該注釋類型的成員的默認(rèn)值。

4.5 運(yùn)行時(shí)支持類

在大多數(shù)情況下,使用 Javassist 修改類不需要運(yùn)行 Javassist。 但是,Javassist 編譯器生成的某些字節(jié)碼需要運(yùn)行時(shí)支持類,這些類位于 javassist.runtime 包中(有關(guān)詳細(xì)信息,請(qǐng)閱讀該包的API文檔)。請(qǐng)注意,javassist.runtime 是修改的類時(shí)唯一可能需要使用的包。 修改類的運(yùn)行時(shí)不會(huì)再使用其他的 Javassist 類。

4.6 導(dǎo)入(Import)

源代碼中的所有類名都必須是完整的(必須包含包名,java.lang 除外)。例如,Javassist 編譯器可以解析 Object 以及 java.lang.Object。

要告訴編譯器在解析類名時(shí)搜索其他包,請(qǐng)?jiān)?ClassPool中 調(diào)用 importPackage()。 例如,

ClassPool pool = ClassPool.getDefault();
pool.importPackage("java.awt");
CtClass cc = pool.makeClass("Test");
CtField f = CtField.make("public Point p;", cc);
cc.addField(f);

第二行導(dǎo)入了 java.awt 包。 因此,第三行不會(huì)拋出異常。 編譯器可以將 Point 識(shí)別為java.awt.Point。

注意 importPackage() 不會(huì)影響 ClassPool 中的 get() 方法。只有編譯器才考慮導(dǎo)入包。 get() 的參數(shù)必須是完整類名。

4.7 限制 (Limitations)

在目前實(shí)現(xiàn)中,Javassist 中包含的 Java 編譯器有一些限制:

  • J2SE 5.0 引入的新語(yǔ)法(包括枚舉和泛型)不受支持。注釋由 Javassist 的低級(jí) API 支持。 參見 javassist.bytecode.annotation 包(以及 CtClass 和 CtBehavior 中的 getAnnotations())。對(duì)泛型只提供部分支持。更多信息,請(qǐng)參閱后面的部分;
  • 初始化數(shù)組時(shí),只有一維數(shù)組可以用大括號(hào)加逗號(hào)分隔元素的形式初始化,多維數(shù)組還不支持;
  • 編譯器不能編譯包含內(nèi)部類和匿名類的源代碼。 但是,Javassist 可以讀取和修改內(nèi)部/匿名類的類文件;
  • 不支持帶標(biāo)記的 continue 和 break 語(yǔ)句;
  • 編譯器沒(méi)有正確實(shí)現(xiàn) Java 方法調(diào)度算法。編譯器可能會(huì)混淆在類中定義的重載方法(方法名稱相同,查參數(shù)列表不同)。例如:
class A {} 
class B extends A {} 
class C extends B {} 
class X { 
    void foo(A a) { .. } 
    void foo(B b) { .. } 
}

如果編譯的表達(dá)式是 x.foo(new C()),其中 xX 的實(shí)例,編譯器將產(chǎn)生對(duì) foo(A) 的調(diào)用,盡管編譯器可以正確地編譯 foo((B) new C()) 。

  • 建議使用 # 作為類名和靜態(tài)方法或字段名之間的分隔符。 例如,在常規(guī) Java 中,
javassist.CtClass.intType.getName()

在 javassist.CtClass 中的靜態(tài)字段 intType 指示的對(duì)象上調(diào)用一個(gè)方法 getName()。 在Javassist 中,用戶也可以寫上面的表達(dá)式,但是建議寫成這樣:

javassist.CtClass#intType.getName()

使編譯器可以快速解析表達(dá)式。


上一篇:Javassist 使用指南(一)
下一篇:Javassist 使用指南(三)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容