天天看點

Javassist-官方文檔翻譯

因為每個markdown的标準不太一樣,裡面格式有些可能是亂的,原版md可以到該項目檢視

https://github.com/IndustriousSnail/javassist-learn
           

文章目錄

    • 一、讀寫位元組碼
      • 1. 擷取類檔案對象CtClass
      • 2. 擷取位元組碼
      • 3. 定義一個新的類
      • 4. 當機類
      • 5. 類路徑搜尋
    • 二、ClassPool詳解
      • 1. ClassPool簡介
      • 2. 避免記憶體溢出
      • 3. 級聯ClassPool
      • 4. 更改類名的方式定義新類
      • 5. 重命名當機類的方式定義新類
    • 三、Class loader詳解
      • 3.1. CtClass的 toClass() 方法
      • 3.2 Java中的類加載
      • 3.3 使用javassist.Loader
      • 3.4 自定義一個類加載器
      • 3.5 修改系統類
      • 3.6 運作期重新加載一個類
    • 四、内省(introspection)和定制(customization)
      • 簡介
      • 4.1 在方法的開頭和結尾插入代碼。
        • 4.1.1 $0, $1, $2, ...
        • 4.1.2 $args
        • 4.1.3 $$
        • 4.1.3 $cflow
        • 4.1.4 $r
        • 4.1.5 $w
        • 4.1.6 $_
        • 4.1.7 $sig
        • 4.1.8 $type
        • 4.1.9 $class
        • 4.1.10 addCatch()
      • 4.2 修改方法體
      • 4.2.1 修改現有的表達式
      • 4.2.2 javassist.expr.MethodCall
        • 4.2.3 javassist.expr.ConstructorCall
        • 4.2.4 javassist.expr.FieldAccess
        • 4.2.5 javassist.expr.NewExpr
        • 4.2.6 javassist.expr.NewArray
        • 4.2.7 javassist.expr.Instanceof
        • 4.2.8 javassist.expr.Cast
        • 4.2.9 javassist.expr.Handler
      • 4.3 增加新方法或新屬性
        • 4.3.1 增加新方法
        • 4.3.2 互相遞歸方法
        • 4.3.3 增添屬性
        • 4.3.4 删除屬性
      • 4.4 注解
      • 4.5 運作時類支援
      • 4.6 Import
      • 4.7 局限性
    • 五、位元組碼API
      • 簡介
      • 5.1 擷取 ClassFile 對象
      • 5.2 增添或删除成員
      • 5.3 周遊方法體
      • 5.4 生成位元組碼序列
      • 5.5 注解(Meta tags)
    • 六、泛型
    • 七、可變參數(int... args)
    • 八、J2ME
    • 九、拆箱和裝箱
    • 十、Debug

一、讀寫位元組碼

1. 擷取類檔案對象CtClass

Javassist是一個用于處理Java位元組碼的庫。Java位元組碼存儲在一個class結尾的二進制檔案中。每一個class檔案都包含了一個Java類或接口。

javassist.CtClass是class檔案的一個抽象代表。一個CtClass(編譯期類)對象處理一個class檔案。例如:

// 見chapter.one.Test1
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
           

這個程式首先定義了個ClassPool對象,它控制着位元組碼的修改。ClassPool對象是CtClass對象的一個容器,它代表一個Class檔案。它會讀取Class(test.Rectangle)檔案,然後構造一個CtClass對象。為了修改一個類,使用者必須用ClassPool對象的get()方法來擷取CtClass對象。上面展示的例子中,CtClass的執行個體cc代表類test.Rectanle。ClassPool執行個體通過 getDefault() 方法執行個體化,它采用預設的搜尋路徑方式。

從實作的角度看,ClassPool是CtClass對象的一個Hash表,ClassPool使用類名作為鍵。當使用classPool.get() 方法時,會搜尋Hash表,根據類名找出相應的CtClass對象。如果該對象沒找到,就會讀取類檔案,然後構造一個CtClass對象,将其存到Hash表中,并傳回結果。

CtClass對象可以被修改(第四章會詳細介紹)。上面的例子中,它将test.Point作為自己的父類。在調用writeFile() 後,該修改就會反映到源class檔案中。

2. 擷取位元組碼

writeFile() 将CtClass對象轉化為一個Class檔案,并把它寫到本地磁盤上。Javassist也提供了一個方法,用于直接擷取被修改的位元組碼。可以調用toBytecode() 方法擷取:

byte[] b = cc.toBytecode();
           

你也可以直接加載CtClass:

Class clazz = cc.toClass();
           

toClass() 請求目前線程的上下文類加載器來加載CtClass代表的class檔案,它傳回java.lang.Class對象。更多細節見第三章。

3. 定義一個新的類

要定義一個新的類,必須使用ClassPool對象,調用其makeClass() 方法:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
           

這段代碼定義了一個類名為Point的類,它沒有任何成員。Point的成員方法可以通過聲明在CtNewMethod中的工廠方法來建立,使用CtClass中的addMethod() 方法可以實作。

makeClass() 不能建立一個新的接口,需要用makeInterface()。接口的成員方法是使用CtNewMethod的abstractMethod() 。注意接口的方法是抽象方法。

4. 當機類

如果一個CtClass對象已經轉化成了class檔案,比如通過writeFile() 、toClass() 、 toBytecode() , Javassist會當機CtClass對象。之後對于CtClass對象的修改都是不允許的。這個是為了警告開發者,他們嘗試修改的Class檔案已經被加載了,JVM不允許再次加載該Class。

當機的類可以如果想要修改,可以進行解凍,這樣就允許修改了,如下:

CtClasss cc = ...;
    :
cc.writeFile();  // 會引起類當機
cc.defrost();   // 解凍
cc.setSuperclass(...);    // OK 因為這個類已經被解凍了
           

在defrost() 被調用之後,該CtClass對象可以再次被修改。

如果ClassPool.doPruning被設定為true,當CtClass被當機時,Javassist會修剪它的資料結構。為了減少記憶體消耗,會删除那個對象中不需要的屬性(attribute_info structures)。例如,Code_attribute結構(方法體)會被删除。是以,在CtClass對象被修剪之後,方法的位元組碼是不可通路的,除了方法名稱,簽名和注釋(我也不知道這裡的annotations指的是注解還是注釋)。被修剪的CtClass對象不能再次被解凍(defrost)。ClassPool.doPruning 的預設是false.

CtClasss cc = ...;
cc.stopPruning(true);
    :
cc.writeFile();     // 轉化為一個Class檔案
// cc 沒有被修剪.
           

該CtClass對象cc沒有被修剪。是以它還可以在調用writeFile() 之後調用defrost() 解凍。

在Debug的時候,你可能想暫停修剪和當機,然後把修改後的class檔案寫到磁盤上, 可以使用**debugWriteFile()**方法來達到目的。 它會停止修剪,然後寫Class檔案,并且再次開始修剪(如果一開始就開始修剪的話)。

5. 類路徑搜尋

ClassPool.getDefault 預設會搜尋JVM下面相同路徑的類,并傳回ClassPool。但是,如果一個程式運作在Web應用伺服器上,像JBoss和Tomcat那種,ClassPool對象可能就找不到使用者指定的類了,因為web應用服務使用了多個系統類加載器。這種情況下,需要給ClassPool注冊一個額外的Class路徑。如下:

pool.insertClassPath(new ClassClassPath(this.getClass()));  // 假設pool是ClassPool的一個執行個體
           

這句代碼注冊了一個類的類路徑,這個類是this指向的那個類。你可以使用任意Class代替this.getClass()。

你也可以注冊一個檔案夾作為類路徑。例如,下面這段代碼增添可以了檔案夾**/usr/local/javalib**到搜尋路徑中:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");
           

搜尋路徑不僅可以是目錄,甚至可以是URL:

ClassPool pool = ClassPool.getDefault();
ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist.");
pool.insertClassPath(cp);
           

該代碼增添了http://www.javassist.org:80/java/ 到類檔案搜尋路徑下。該URL僅僅搜尋org.javassist. 包下的class檔案。例如,要加載org.javassist.test.Main 這個類,javassist會從這個位址下擷取該類檔案:

http://www.javassist.org:80/java/org/javassist/test/Main.class
           

此外,你也可以直接給ClassPool對象一個byte數組,然後用這個數組建構CtClass對象。要這樣做,用ByteArrayClassPath, 例如:

ClassPool cp = ClassPool.getDefault();
byte[] b = a byte array;
String name = class name;
cp.insertClassPath(new ByteArrayClassPath(name, b));
CtClass cc = cp.get(name);
           

獲得的CtClass對象表示一個由b指定的類檔案定義的類。如果調用get() ,ClassPool會從ByteArrayClassPath中讀取一個Class檔案,指定的Class的名字就是上面的name變量。

如果你不知道這個類的全限定名,你你可以使用ClassPool中的makeClass() :

ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);
           

makeClass() 傳回一個通過輸入流建構出來的CtClass。你可以使用makeClass() 給ClassPool 對象提供一個比較急的Class檔案。如果搜尋路徑包含了一個很大的jar包,這可以提高性能。因為ClassPool對象會一個一個找,它可能會重複搜尋整個jar包中的每一個class檔案。makeClass() 可以優化這個搜尋。makeClass()構造出來的類會儲存在ClassPool對象中,你下次再用的時候,不會再次讀Class檔案。

二、ClassPool詳解

1. ClassPool簡介

ClassPool對象是多個CtClass對象的容器。一旦CtClass對象被建立,它就會永遠被記錄再ClassPool對象中。這是因為編譯器之後在編譯源碼的時候可能需要通路CtClass對象。

例如,假定有一個新方法getter() 被增添到了表示Point類的CtClass對象。稍後,程式會試圖編譯代碼,它包含了對Point方法的getter() 調用,并會使用編譯後代碼作為一個方法的方法體,它将會被增添到另一個類Line中。如果表示Point類的CtClass對象丢了的話,編譯器就不能編譯調用getter() 的方法了(注意:原始類定義中不包含getter() )。是以,為了正确編譯這樣一個方法調用,ClassPool在程式過程中必須示種包含所有的CtClass對象。

ClassPool classPool = ClassPool.getDefault();
CtClass point = classPool.makeClass("Point");
point.addMethod(getterMethod);  // Point增添了getter方法
CtClass line = ...; // Line方法
// line 調用point的getter方法
           

2. 避免記憶體溢出

某種特定的ClassPool可能造成巨大的記憶體消耗,導緻OOM,比如CtClass對象變得非常的(這個發生的很少,因為Javassist已經嘗試用不同的方法減少記憶體消耗了,比如當機類)。為了避免該問題,你可以從ClassPool中移除不需要的CtClass對象。隻需要調用CtClass的detach() 方法就行了:

CtClass cc = ... ;
cc.writeFile();
cc.detach();  // 該CtClass已經不需要了,從ClassPool中移除
           

在調用detach() 之後,這個CtClass對象就不能再調用任何方法了。但是你可以依然可以調用classPool.get() 方法來建立一個相同的類。如果你調用get() ,ClassPool會再次讀取class檔案,然後建立一個新的CtClass對象并傳回。

另一種方式是new一個新的ClassPool,舊的就不要了。這樣舊的ClassPool就會被垃圾回收,它的CtClass也會被跟着垃圾回收。可以使用以下代碼完成:

ClassPool cp = new ClassPool(true);  // true代表使用預設路徑
// 如果需要的話,可以用appendClassPath()添加一個額外的搜尋路徑。
           

上面這個new ClassPool和ClassPool.getDefault() 的效果是一樣。注意,ClassPool.getDefault() 是一個單例的工廠方法,它隻是為了友善使用者建立提供的方法。這兩種建立方式是一樣的,源碼也基本是一樣的,隻不過**ClassPool.getDefault()**是單例的。

注意,new ClassPool(true) 是一個很友善的構造函數,它構造了一個ClassPool對象,然後給他增添了系統搜尋路徑。它構造方法的調用就等同于下面的這段代碼:

ClassPool cp = new ClassPool();
cp.appendSystemPath();  // 你也可以通過appendClassPath()增添其他路徑
           

3. 級聯ClassPool

如果一個程式運作在Web應用伺服器上,你可能需要建立多個ClassPool執行個體。為每一個類加載器(ClassLoader)建立一個ClassPool(也就是容器)。這時程式在建立ClassPool對象的時候就不能再用getDefault() 了,而是要用ClassPool的構造函數。

多個ClassPool對象可以像java.lang.ClassLoader那樣進行級聯。例如:

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
           

如果調用了child.get() ,child的ClassPool首先會代理parent的ClassPool,如果parent的ClassPool中沒有找到要找的類,才會試圖到child中的**./classes**目錄下找。

如果child.childFirstLookup設定為了true,child的ClassPool就會首先到自己路徑下面找,之後才會到parent的路徑下面找。

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath();         // 這預設使用相同的類路徑
child.childFirstLookup = true;    // 改變child的行為。
           

4. 更改類名的方式定義新類

一個“新類”可以從一個已經存在的類copy出來。可以使用以下代碼:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
           

這段代碼首先擷取了Point的CtClass對象。然後調用setName() 方法給對象一個新的名字Pair。在這個調用之後,CtClass表示的類中的所有Point都會替換為Pair。類定義的其他部分不會變。

既然setName() 改變了ClassPool對象中的記錄。從實作的角度看,ClassPool是一個hash表,setName() 改變了關聯這個CtClass對象的key值。這個key值從原名稱Point變為了新名稱Pair。

是以,如果之後調用get(“Point”) ,就不會再傳回上面的cc引用的對象了。ClassPool對象會再次讀取class檔案,然後構造一個新的CtClass對象。這是因為Point這個CtClass在ClassPool中已經不存在了。請看下面代碼:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point");   // 此時,cc1和cc是完全一樣的。
cc.setName("Pair");
CtClass cc2 = pool.get("Pair");    // cc2和cc是完全一樣的
CtClass cc3 = pool.get("Point");   // cc3和cc是不一樣的,因為cc3是重新讀取的class檔案
           

cc1和cc2引用的是相同的執行個體,和cc指向的是同一位址。但是,cc3卻不是。注意,在執行cc.setName(“Pair”) 之後,cc和cc1引用的是同一位址,是以它們的CtClass都是代表Pair類。

ClassPool對象用于維護CtClass對象和類之間的一一映射關系。Javassist不允許兩個不同的CtClass對象代表相同的類,除非你用兩個ClassPool。這個是程式轉換一緻性的重要特性。

要建立ClassPool的副本,可以使用下面的代碼片段(這個上面已經提到過了):

ClassPool cp = new ClassPool(true);
           

如果你又兩個ClassPool對象,那麼你就可以從這兩個對象中擷取到相同class檔案但是不同的CtClass對象。你可以對那兩個CtClass進行不同方式的修改,然後生成兩個版本的Class。

5. 重命名當機類的方式定義新類

一旦CtClass對象轉化為Class檔案後,比如writeFile() 或是 toBytecode() 之後,Javassist會拒絕CtClass對象進一步的修改。是以,在CtClass對象轉為檔案之後,你将不能再通過setNme() 的方式将該類拷貝成一個新的類了。比如,下面的這段錯誤代碼:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair");    // 錯, 因為cc已經調用了writeFile()
           

為了解除這個限制,你應該調用ClassPool 的 getAndRename() 方法。 例如:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair"); 
           

如果調用了getAndRename,ClassPool首先為了建立代表Pair的CtClass而去讀取Point.class。然而,它在記錄CtClass到hash表之前,會把CtClass由Point重命名為Pair。是以getAndRename() 可以在writeFile() 或 toBytecode() 之後執行。

三、Class loader詳解

如果一開始你就知道要修改哪個類,那麼最簡單的方式如下:

  • 1.調用ClassPool.get() 來擷取一個CtClass對象。
  • 2.修改它
  • 3.調用writeFile() 或 toBytecode() 來擷取一個修改後的class檔案

如果一個類是否要被修改是在加載時确定的,使用者就必須讓Javassist和類加載器協作。Javassist可以和類加載器一塊兒使用,以便于可以在加載時修改位元組碼。使用者可以自定義類加載器,也可以使用Javassist提供好的。

3.1. CtClass的 toClass() 方法

CtClass提供了一個友善的方法toClass(), 它會請求目前線程的上下文類加載器,讓其加載CtClass對象所代表的那個類。要調用這個方法,必須要擁有權限。此外,該方法還會抛出SecurityException異常。

使用toClass() 方法樣例:

public class Hello {
    public void say() {
        System.out.println("Hello");
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        ClassPool cp = ClassPool.getDefault();
        CtClass cc = cp.get("Hello");
        // 擷取say方法
        CtMethod m = cc.getDeclaredMethod("say");
        // 在方法第一行前面插入代碼
        m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
        Class c = cc.toClass();
        Hello h = (Hello)c.newInstance();
        h.say();
    }
}
           

Test.main() 在Hello的say() 方法的方法體中插入了println() 的調用。然後建構了被修改後的Hello的執行個體,然後調用了該執行個體的say() 方法。

注意,上面這段程式有一個前提,就是Hello類在調用toClass() 之前沒有被加載過。否則,在toClass() 請求加載被修改後的Hello類之前,JVM就會加載原始的Hello類。是以,加載被修改後的Hello類就會失敗(抛出LinkageError)。例如:

public static void main(String[] args) throws Exception {
    Hello orig = new Hello();
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("Hello");
    Class c = cc.toClass();  // 這句會報錯
}
           

main函數的第一行加載了Hello類,cc.toClass() 這行就會抛出異常。原因是類加載器不能同時加載兩個不同版本的Hello類。

如果你的程式運作在JBOSS或Tomcat的應用伺服器上,那麼你再用toClass() 就有點不合适了。這種情況下,将會抛出ClassCastException異常。為了避免這個異常,你必須給toClass() 一個合适的類加載器。例如,假設bean是你的會話bean對象,那麼這段代碼:

CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
           

這段代碼可以正常運作。你應該給toClass() 的類加載器是加載你程式的加載器(上面的例子中,就是bean對象的class的類加載器)。

toClass() 已經很友善了。你要是想更複雜的類加載器,你應該自定義類加載器。

3.2 Java中的類加載

在Java中,多個類加載器可以共存,它們可以建立自己的命名空間。不同的類加載器能夠加載有着相同類名的不同的類檔案。被加載過的兩個類會被視為不同的東西。這個特點可以讓我們在一個JVM中運作多個應用程式,盡管它們包含了有着相同名稱的不同的類。

JVM不允許動态重新加載一個類。一旦類加載加載過一個類之後,在運作期就不能在加載該類的另一個被修改過的版本。是以,你不能在JVM加載過一個類之後修改它的定義。但是,JPDA(Java Platform Debugger Architecture)提供了重新加載類的一些能力。詳細請看3.6

如果兩個不同的類加載器加載裡一個相同的Class檔案,那麼JVM會生成兩個不同的Class,雖然它們擁有相同的名字和定義。這兩個Class會被視為兩個不同的東西。因為這兩個Class不是完全相同的,是以一個Class的執行個體不能指派給另一個Class的變量。這兩個類之間的類型轉換會失敗,抛出ClassCastException異常。

例如,下面這個代碼片段就會抛出該異常:

MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj;    // 這裡總是會抛出ClassCastException異常.
           

Box類被兩個類加載器所加載。假定CL類加載器加載了這段代碼片段。因為該代碼中引用了MyClassLoader,Class,Object,是以CL也會加載這些類(除非它代理了其它啊類加載器)。是以,b 變量的類型是CL加載的Box。但是obj變量的類型是myLoader加載的Box,雖然都是Box,但是不一樣。是以,最後一段代碼一定會抛出ClassCastException,因為b和obj是兩個不同版本的Box。

多個類加載形成了一個樹型結構。除了啟動加載器之外,其他的類加載器都有一個父類加載,子類加載器通常由父類加載器加載。由于加載類的請求可以沿着加載器的層次結構進行委托,是以你請求加載類的加載器,并不一定真的是由這個加載器加載的,也可能換其他加載器加載了。是以(舉例),請求加載類C的加載器可能不是真正加載類C的加載器。不同的是,我們将前面的加載器稱為C的發起者(initiator),後面的加載器稱為C實際的加載器(real loader)。

除此之外,如果類加載器CL請求加載一個類C(C的發起者)委托給了它的父類加載器PL,那麼類加載器CL也不會加載類C定義中引用的任何其他類。對于那些類,CL不是它們的發起者,相反,父加載器PL則會稱為它們的發起者,并且回去加載它們。類C定義中引用的類,由類C的實際的加載器去加載。

要了解上面的行為,可以參考下面代碼:

public class Point {    // PL加載該類
    private int x, y;
    public int getX() { return x; }
        :
}

public class Box {      // L是發起者,但實際的加載器是PL
    private Point upperLeft, size;
    public int getBaseX() { return upperLeft.x; }
        :
}

public class Window {    // 該類被加載器L加載
    private Box box;
    public int getBaseX() { return box.getBaseX(); }
}
           

假定類加載器L加載Window類。加載Window的發起者和實際加載者都是L。因為Window的定義引用了類Box,是以JVM會讓 L去加載Box類。這裡,假定L将該任務委托給了父類加載器PL,是以加載Box的發起者是L,但實際加載者是PL。這種情況下,PL作為Box的實際加載者,就會去加載Box中定義中引用的Point類,是以Point的發起者和實際加載者都是PL。是以加載器L從來都沒有請求過加載Point類。

把上面的例子稍微改一下:

public class Point {
    private int x, y;
    public int getX() { return x; }
        :
}

public class Box {      // 發起者是L,但實際加載者是PL
    private Point upperLeft, size;
    public Point getSize() { return size; }
        :
}

public class Window {    // Window由加載器L加載
    private Box box;
    public boolean widthIs(int w) {  // 增加了方法,方法中有對Point類的引用。
        Point p = box.getSize();
        return w == p.getX();
    }
}
           

上面中,Window也引用了Point。這樣,如果加載器L需要加載Point的話,L也必須委托給PL。你必須避免讓兩個類加載器重複加載同一個類。兩個加載器中的一個必須委托給另一個加載器。

如果當Point被加載時,L沒有委托給PL,那麼widthIs()就會抛出ClassCastException。因為Window裡的Point是L加載的,而Box中的Point是PL加載器加載的。你用box.getSize() 傳回的PL.Point給L的Point,那麼就會JVM就會認為它們是不同的執行個體,進而抛出異常。

這樣有些不友善,但是需要有這種限制。比如:

Point p = box.getSize();
           

如果這條語句沒有抛出異常,那麼Window的代碼就有可能打破Point的封裝。例如,PL加載的Point的x變量是private,但是L加載器加載的Point的x變量是public(下面的代碼定義),那麼不就打破了封裝定義。

public class Point {
    public int x, y;    // not private
    public int getX() { return x; }
}
           

要是想了解更多關于JAVA類加載器的細節,可以參考下面這個論文:

Sheng Liang and Gilad Bracha, "Dynamic Class Loading in the Java Virtual Machine", 
ACM OOPSLA'98, pp.36-44, 1998.
           

3.3 使用javassist.Loader

Javassist提供了一個類加載器javasist.Loader,該加載器使用一個javassist.ClassPool對象來讀取類檔案。

例如,javassist.Loader可以加載一個被Javassist修改過的特定類:

import javassist.*;
import test.Rectangle;

public class Main {
  public static void main(String[] args) throws Throwable {
     ClassPool pool = ClassPool.getDefault();
     Loader cl = new Loader(pool);

     CtClass ct = pool.get("test.Rectangle");
     ct.setSuperclass(pool.get("test.Point"));

     Class c = cl.loadClass("test.Rectangle");
     Object rect = c.newInstance();
  }
}
           

這段 程式修改了test.Rectangle,将它的父類設定為了test.Point。然後程式加載了修改後的類,并且建立了test.Rectangle的一個新執行個體。

如果使用者想根據需要在類被加載的時候修改類,那麼使用者可以增添一個事件監聽器給javassist.Loader。該事件監聽器會在類加載器加載類時被通知。事件監聽器必須實作下面這個接口:

public interface Translator {
    public void start(ClassPool pool)
        throws NotFoundException, CannotCompileException;
    public void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException;
}
           

當使用javassist.Loader的addTranslator() 方法增添事件監聽器時,start() 方法就會被調用。在javassist.Loader加載類之前,onLoad() 方法就會被調用。你可以在onLoad() 方法中修改要加載的類的定義。

例如,下面的事件監聽器就在類被加載之前把它們都修改成public類。

public class MyTranslator implements Translator {
    void start(ClassPool pool)
        throws NotFoundException, CannotCompileException {}
    void onLoad(ClassPool pool, String classname)
        throws NotFoundException, CannotCompileException
    {
        CtClass cc = pool.get(classname);
        cc.setModifiers(Modifier.PUBLIC);
    }
}
           

注意onLoad()不必調用toBytecode()或writeFile(),因為javassist.Loader會調用這些方法來擷取類檔案。

要想運作一個帶有Mytranslator對象的application(帶main方法,可以運作的)類MyApp,可以這樣寫:

import javassist.*;

public class Main2 {
  public static void main(String[] args) throws Throwable {
     Translator t = new MyTranslator();
     ClassPool pool = ClassPool.getDefault();
     Loader cl = new Loader();
     cl.addTranslator(pool, t);
     cl.run("MyApp", args);
  }
}
           

然後這樣運作這個程式:

> java Main2 arg1 arg2...
           

這樣MyApp和其他的應用程式類就會被MyTranslator轉換了。

注意,像MyApp這樣的應用類不能通路加載器的類,不如Main2,MyTranslator和ClassPool。因為他們是被不同的加載器加載的。應用類時javassist.Loader加載的,然而像Main2這些是被預設的java類加載器加載的。

javassist.Loader搜尋類的順序和java.lang.ClassLoader.ClassLoader不同。JavaClassLoader首先會委托父加載器進行加載操作,父加載器找不到的時候,才會由子加載器加載。而javassist.Loader首先嘗試加載類,然後才會委托給父加載器。隻有在下面這些情況才會進行委托:

  • 調用get()方法後在ClassPool對象中找不到
  • 使用delegateLoadingOf() 方法指定要父類加載器去加載

這個搜尋順序機制允許Javassist加載修改後的類。然而,如果它因為某些原因找不到修改後的類的話,就會委托父加載器去加載。一旦該類被父加載器加載,那麼該類中引用的類也會用父加載器加載,并且它們不能再被修改了。回想下,之前類C的實際加載器加載了類C所有引用的類。如果你的程式加載一個修改過的類失敗了,那麼你就得想想是否那些類是否使用了被javassist.Loader加載的類。

3.4 自定義一個類加載器

一個簡單的類加載器如下:

import javassist.*;

public class SampleLoader extends ClassLoader {
    /* Call MyApp.main().
     */
    public static void main(String[] args) throws Throwable {
        SampleLoader s = new SampleLoader();
        Class c = s.loadClass("MyApp");
        c.getDeclaredMethod("main", new Class[] { String[].class })
         .invoke(null, new Object[] { args });
    }

    private ClassPool pool;

    public SampleLoader() throws NotFoundException {
        pool = new ClassPool();
        pool.insertClassPath("./class"); // MyApp.class must be there.
    }

    /* Finds a specified class.
     * The bytecode for that class can be modified.
     */
    protected Class findClass(String name) throws ClassNotFoundException {
        try {
            CtClass cc = pool.get(name);
            // modify the CtClass object here
            byte[] b = cc.toBytecode();
            return defineClass(name, b, 0, b.length);
        } catch (NotFoundException e) {
            throw new ClassNotFoundException();
        } catch (IOException e) {
            throw new ClassNotFoundException();
        } catch (CannotCompileException e) {
            throw new ClassNotFoundException();
        }
    }
}
           

MyApp是一個應用程式。要執行這段程式,首先要放一個class檔案到 ./class 目錄下,該目錄不能包含在類搜尋路徑下。否則,MyApp.class将會被預設的系統類加載器加載,也就是SampleLoader的父類加載器。你也可以把insertClassPath中的 ./class 放入構造函數的參數中,這樣你就可以選擇自己想要的路徑了。 運作java程式:

> java SampleLoader
           

類加載器加載了類MyApp(./class/MyApp.class),并且調用了MyApp.main() ,并傳入了指令行參數。

這是使用Javassist最簡單的方式。如果你想寫個更複雜的類加載器,你可能需要更多的java類加載機制的知識。例如,上面的程式把MyApp的命名空間和SampleLoader的命名空間是分開的,因為它們兩個類是由不同的類加載器加載的。是以,MyApp不能直接通路SampleLoader類。

3.5 修改系統類

除了系統類加載器,系統類不能被其他加載器加載,比如java.lang.String。是以,上面的SampleLoader和javassist.Loader在加載期間不能修改系統類。

如果你的程式非要那麼做,請“靜态的”修改系統類。例如,下面的程式給java.lang.String增添了hiddenValue屬性。

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("java.lang.String");
CtField f = new CtField(CtClass.intType, "hiddenValue", cc);
f.setModifiers(Modifier.PUBLIC);
cc.addField(f);
cc.writeFile(".");
           

這個程式會生成一個檔案 ./java/lang/String.class

用修改過的String類運作一下你的程式MyApp,按照下面:

> java -Xbootclasspath/p:. MyApp arg1 arg2...
           

假定MyApp的代碼是這樣的:

public class MyApp {
    public static void main(String[] args) throws Exception {
        System.out.println(String.class.getField("hiddenValue").getName());
    }
}
           

如果被修改的String正常加載的話,MyApp就會列印hiddenValue。

應用最好不要使用該技術去重寫rt.jar中的内容,這樣會違反Java 2 Runtime Environment binary code 協定。

3.6 運作期重新加載一個類

啟動JVM時,如果開啟了JPDA(Java Platform Debugger Architecture),那麼class就可以動态的重新加載了。在JVM加載一個類之後,舊的類可以被解除安裝,然後重新加載一個新版的類。意思就是,類的定義可以在運作期動态的修改。但是,新類一定要能和舊類互相相容。JVM不允許兩個版本存在模式的改變,它們需要有相同的方法和屬性。

Javassist提供了一個很友善的類,用于在運作期改變類。想了解更多資訊,可以看javassist.tools.HotSwapper的API文檔

四、内省(introspection)和定制(customization)

簡介

CtClass 提供了自省的方法。Javassist的自省能力是能夠相容Java的反射API的。CtClass提供了getName(),getSuperclass(),getMethods() 等等方法。它也提供了修改類定義的方法。它允許增添一個新的屬性,構造函數以及方法。甚至可以改變方法體。

CtMethod*對象代表一個方法。CtMethod提供了一些修改方法定義的方法。注意,如果一個方法是從父類繼承過來的,那麼相同的 CtMethod對象也會代表父類中聲明的方法。CtMethod對象會對應到每一個方法定義中。

例如,如果Point類聲明了move() 方法,并且它的子類ColorPoint 沒有重寫move() 方法,那麼Point和ColorPoint的move() 方法會具有相同的CtMethod對象。如果用CtMethod修改了方法定義,那麼該就該就會在兩個類中都生效。如果你隻想修改ColorPoint中的move() 方法,你必須要增添一個Point.move() 方法的副本到ColorPoint中去。可以使用CtNewethod.copy() 來擷取CtMethod對象的副本。

Javassist不允許移除方法或者屬性,但是允許你重命名它們。是以如果你不再需要方法或屬性的時候,你應該将它們重命名或者把它們改成私有的,可以調用CtMethod的setName() 和 setModifiers() 來實作。

Javassist不允許給一個已經存在的方法增添額外的參數。如果你非要這麼做,你可以增添一個同名的新方法,然後把這個參數增添到新方法中。例如,如果你想增添一個額外的int參數給newZ 方法:

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

假設這個是在Point類中的,那麼你應該增添以下的代碼到Point中

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

Javassist也提供了一個底層API,用于直接編輯一個原生class檔案。例如,CtClass中的getClassFile就會傳回一個ClassFile對象,它代表了一個原生Class檔案。CtMethod中的getMethodInfo() 會傳回一個MethodInfo對象,它代表一個Class檔案中的method_info結構。底層API使用了JVM的一些特定詞彙,使用者需要了解class檔案和位元組碼的一些知識。更多詳情,可以參考第五章。

隻要辨別符是$開頭的,那麼在修改class檔案的時候就需要javassist.runtime包用于運作時支援。那些特定的辨別符會在下面進行說明。要是沒有辨別符,可以不需要javassist.runtime和其他的運作時支援包。更多詳細内容,可以參考javassist.runtime包的API文檔。

4.1 在方法的開頭和結尾插入代碼。

CtMethod和CtConstructor提供了insertBefore(),insertAfter(),addCatch() 方法。它們被用于在已經存在的方法上面插入代碼片段。使用者可以把它們的代碼以文本的形式寫進去。Javassist包含了一個簡單的編譯器,可以處理這些源碼文本。它能夠把這些代碼編譯成位元組碼,然後将它們内嵌到方法體中。

往指定行插入代碼也是有可能的(前提是class檔案中包含行号表)。CtMethod和CtConstructor中的insertAt() 就可以将代碼插入指定行。它會編譯代碼文本,然後将其編譯好的代碼插入指定行。

insertBefore(),insertAfter(),addCatch(),insertAt() 這些方法接受一個字元串來表示一個語句(statements)或代碼塊(block)。一句代碼可以是一個控制結構,比如if、while,也可以是一個以分号(;)結尾的表達式。代碼塊是一組用 {} 括起來的語句。是以,下面的每一行代碼都是一個合法的語句或代碼塊。

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

語句和代碼塊都可以引用屬性或方法。如果方法使用 -g 選項(class檔案中包含局部變量)進行編譯,它們也可以引用自己插入方法的參數。否則,它們隻能通過特殊的變量 $0,$1,$2… 來通路方法參數,下面有說明。雖然在代碼塊中聲明一個新的局部變量是允許的,但是在方法中通路它們确是不允許的。然而,如果使用 -g 選項進行編譯, 就允許通路。

傳遞到insertBefore(), insertAfter() 等方法中的String字元串會被Javassist的編譯器編譯。因為該編譯器支援語言擴充,是以下面的這些以$開頭的辨別符就具有了特殊意義:

辨別符 含義 英語含義
$0, $1, $2, … this 和實參 this and actual parameters
$args 參數數組。$args 的類型是 Object[] An array of parameters. The type of $args is Object[].
$$ 所有實參,例如m($$)等同于m($1,$2,…) All actual parameters.For example, m($$) is equivalent to m($1,$2,…)
$cflow(…) cflow變量 cflow variable
$r 傳回值類型。用于強制類型轉換表達式 The result type. It is used in a cast expression.
$w 包裝類型。用于強制類型轉換表達式 The wrapper type. It is used in a cast expression.
$_ 結果值 The resulting value
$sig java.lang.Class對象的數組,表示參數類型 An array of java.lang.Class objects representing the formal parameter types.
$type java.lang.Class對象的數組,表示結果類型 A java.lang.Class object representing the formal result type.
$class java.lang.Class對象的數組,表示目前被編輯的類 A java.lang.Class object representing the class currently edited.

4.1.1 $0, $1, $2, …

傳遞給目标方法的參數可以通過**$1,$2,…** 通路,而不是通過原先的參數名稱。$1 代表第一個參數,$2代表第二個參數,以此類推。那些變量的類型和參數的類型是一樣的。$0代表this,如果是靜态方法的話,$0不能用。

假定有一個Point類如下:

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

要想在調用move() 時列印dx和dy的值,可以執行下面代碼:

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();
           

注意需要用 {} 括起來,如果隻有一行語句,可以不用括。

修改後的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 和 $2 分别被dx和dy給替換了。

$1, $2, $3 是可以被更新的,如果它們被賦予了新值,那麼它們對應的變量也會被賦予新值。

4.1.2 $args

變量 a r g ∗ ∗ 表 示 所 有 參 數 的 一 個 數 組 。 數 組 中 的 類 型 都 是 ∗ ∗ O b j e c t ∗ ∗ 。 如 果 參 數 是 基 本 數 據 類 型 比 如 ∗ ∗ i n t ∗ ∗ , 那 麼 該 參 數 就 會 被 轉 換 成 包 裝 類 型 比 如 ∗ ∗ j a v a . l a n g . I n t e g e r ∗ ∗ , 然 後 存 儲 到 ∗ ∗ arg** 表示所有參數的一個數組。數組中的類型都是 **Object** 。如果參數是基本資料類型比如**int**,那麼該參數就會被轉換成包裝類型比如**java.lang.Integer**,然後存儲到 ** arg∗∗表示所有參數的一個數組。數組中的類型都是∗∗Object∗∗。如果參數是基本資料類型比如∗∗int∗∗,那麼該參數就會被轉換成包裝類型比如∗∗java.lang.Integer∗∗,然後存儲到∗∗args中。是以,$args[0] 就等于 1 ∗ ∗ , 除 非 它 是 個 基 本 類 型 ( i n t 不 等 于 I n t e g e r ) 。 注 意 , ∗ ∗ 1** ,除非它是個基本類型(int不等于Integer)。注意,** 1∗∗,除非它是個基本類型(int不等于Integer)。注意,∗∗args[0] 不等于 $0 。 $0 是this。

如果一個 Object 數組指派給了 $args , 那麼參數的每一個元素都會一一指派。如果某個參數是基本類型,那麼相應的元素必須是包裝類型。該值會從包裝類型自動拆箱轉換成基本資料類型。

4.1.3 $$

$$ 是一個以逗号分隔參數的縮寫。例如,如果move() 方法的參數是3個。那麼:

move($$)
           

就等于:

move($1, $2, $3)
           

如果move() 沒有接受任何參數,那麼move($$) 就等于move() 。

$$ 也可以跟其他參數一起使用,比如你寫這樣一個表達式:

exMove($$, context)
           

這個表達式就等通于下面:

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

$$ 能夠支援泛型表示。一般與**$procced**一起使用,後面會說。

4.1.3 $cflow

$cflow 意思就是控制流(control flow)。該隻讀變量傳回特定方法進行遞歸調用時的深度。

假定CtMethod執行個體cm代表下面這個方法:

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

要使用** c f l o w ∗ ∗ , 首 先 要 聲 明 ∗ ∗ cflow**,首先要聲明** cflow∗∗,首先要聲明∗∗cflow要用于監控fact()** 方法的調用。

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

useCflow() 的參數是聲明**$cflow**變量的辨別符。任何合法的Java名稱都能作為辨別符。是以辨別符也可以包含點(.)。例如,my.Test.fact就是一個合法的辨別符。

那麼, c f l o w ( f a c t ) ∗ ∗ 表 示 該 方 法 遞 歸 調 用 時 的 深 度 。 當 該 方 法 在 方 法 内 部 遞 歸 調 用 時 , 第 一 次 被 調 用 時 ∗ ∗ cflow(fact)** 表示該方法遞歸調用時的深度。當該方法在方法内部遞歸調用時,第一次被調用時** cflow(fact)∗∗表示該方法遞歸調用時的深度。當該方法在方法内部遞歸調用時,第一次被調用時∗∗cflow(fact) 的值是0而不是1。例如:

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

将fact加入了顯示參數的代碼。因為**$cflow(face)** 被檢查,是以如果在内部遞歸調用fact方法,則不會列印參數。

在目前線程的目前最頂層的堆棧幀下, c f l o w ∗ ∗ 的 值 是 ∗ ∗ c m ∗ ∗ 關 聯 的 指 定 方 法 的 堆 棧 深 度 。 ∗ ∗ cflow**的值是**cm**關聯的指定方法的堆棧深度。** cflow∗∗的值是∗∗cm∗∗關聯的指定方法的堆棧深度。∗∗cflow也能夠在其他的方法下面通路。

4.1.4 $r

$r 代表方法的傳回值類型。他必須用于強制轉換表達式中的轉換類型。例如,這是它的一個典型用法:

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

如果傳回值類型是一個基本資料類型,那麼 ( r ) ∗ ∗ 就 會 遵 循 特 殊 的 語 義 。 首 先 , 如 果 被 轉 換 對 象 的 類 型 就 是 基 本 類 型 , 那 麼 ∗ ∗ ( r)** 就會遵循特殊的語義。首先,如果被轉換對象的類型就是基本類型,那麼 **( r)∗∗就會遵循特殊的語義。首先,如果被轉換對象的類型就是基本類型,那麼∗∗(r) 就會基本類型到基本類型的轉換。但是,如果被轉換對象的類型是包裝類型,那麼** r ∗ ∗ 就 會 從 包 裝 類 型 轉 為 基 本 數 據 類 型 。 例 如 , 如 果 返 回 值 類 型 為 ∗ ∗ i n t ∗ ∗ , 那 ∗ ∗ ( r**就會從包裝類型轉為基本資料類型。例如,如果傳回值類型為**int**,那 **( r∗∗就會從包裝類型轉為基本資料類型。例如,如果傳回值類型為∗∗int∗∗,那∗∗(r)** 就會将其從java.lang.Integer轉為int。

如果傳回值類型為void,那麼 ( r ) ∗ ∗ 不 會 進 行 類 型 轉 換 。 它 什 麼 都 不 做 。 然 而 , 如 果 調 用 的 方 法 返 回 值 為 ∗ ∗ v o i d ∗ ∗ 的 話 , 那 麼 ∗ ∗ ( r)** 不會進行類型轉換。 它什麼都不做。然而,如果調用的方法傳回值為**void**的話,那麼 **( r)∗∗不會進行類型轉換。它什麼都不做。然而,如果調用的方法傳回值為∗∗void∗∗的話,那麼∗∗(r) 的結果就是null。例如,如果foo() 方法的傳回值為void,那麼:

$_ = ($r)foo();
           

這是一個合法語句。

類型轉換符 ($r) 在return語句中也是很有用的。即使傳回值類型為void,下面的return語句也是合法的:

return ($r)result;
           

這裡,result是某個本地變量。因為 ($r) 是void的,是以傳回值就被丢棄了。return語句也被視為沒有傳回值,就等同于下面:

return;
           

4.1.5 $w

w ∗ ∗ 表 示 一 個 包 裝 類 型 。 它 必 須 用 于 強 制 類 型 轉 換 表 達 式 中 。 ∗ ∗ ( w** 表示一個包裝類型。它必須用于強制類型轉換表達式中。**( w∗∗表示一個包裝類型。它必須用于強制類型轉換表達式中。∗∗(w) 把一個基本資料類型轉換為包裝類型。例如:

Integer i = ($w)5;
           

所用的包裝類型(Integer)取決于 ($w) 後面表達式的類型。如果表達式類型為double,那麼包裝類型應為java.lang.Double。

如果 ( w ) ∗ ∗ 後 面 的 表 達 式 不 是 基 本 數 據 類 型 的 話 , 那 麼 ∗ ∗ ( w)** 後面的表達式不是基本資料類型的話,那麼 **( w)∗∗後面的表達式不是基本資料類型的話,那麼∗∗(w) 将不起作用。

4.1.6 $_

CtMethod和CtConstructor中的insertAfter() 在方法最後插入代碼時,不隻是 $1, 2.. ∗ ∗ 這 些 可 以 用 , 你 也 可 用 ∗ ∗ 2..** 這些可以用,你也可用** 2..∗∗這些可以用,你也可用∗∗_。

∗ ∗ 表 示 方 法 的 返 回 值 。 而 該 變 量 的 類 型 取 決 于 該 方 法 的 返 回 值 類 型 。 如 果 方 法 的 返 回 值 類 型 為 ∗ ∗ v o i d ∗ ∗ , 那 麼 ∗ ∗ _** 表示方法的傳回值。而該變量的類型取決于該方法的傳回值類型。如果方法的傳回值類型為**void**,那麼 ** ∗​∗表示方法的傳回值。而該變量的類型取決于該方法的傳回值類型。如果方法的傳回值類型為∗∗void∗∗,那麼∗∗_的值是null,類型為Object。

隻有方法不報錯,運作正常的情況下,insertAfter() 中的代碼才會運作。如果你想讓方法在抛出異常的時候也能執行insertAfter() 中的代碼,那你就把該方法的第二個參數asFinally設定為true.

如果方法中抛出了異常,那麼insertAfter() 中的代碼也會在finally語句中執行。這時 ** ∗ ∗ 的 值 是 ∗ ∗ 0 ∗ ∗ 或 ∗ ∗ n u l l ∗ ∗ 。 插 入 的 代 碼 執 行 完 畢 後 , 抛 出 的 異 常 還 會 抛 給 原 來 的 調 用 者 。 注 意 , ∗ ∗ _** 的值是**0**或**null**。插入的代碼執行完畢後,抛出的異常還會抛給原來的調用者。注意,** ∗​∗的值是∗∗0∗∗或∗∗null∗∗。插入的代碼執行完畢後,抛出的異常還會抛給原來的調用者。注意,∗∗_**的值不會抛給原來的調用者,它相當于沒用了(抛異常的時候沒有傳回值)。

4.1.7 $sig

$sig是一個java.lang.Class對象的數組,數組的内容是按找參數順序,記錄每個參數的類型。

4.1.8 $type

$type 是一個java.lang.Class對象,它表示傳回值類型。如果是構造函數,則它是Void.class的引用。

4.1.9 $class

c l a s s ∗ ∗ 值 是 ∗ ∗ j a v a . l a n g . C l a s s ∗ ∗ 對 象 , 代 表 修 改 的 方 法 所 對 應 的 那 個 類 。 ∗ ∗ class** 值是 **java.lang.Class** 對象,代表修改的方法所對應的那個類。** class∗∗值是∗∗java.lang.Class∗∗對象,代表修改的方法所對應的那個類。∗∗class 是 $0 的類型($0是this)。

4.1.10 addCatch()

addCatch() 往方法體插入了的代碼片段會在方法抛出異常的時候執行。在源碼中,你可以用**$e** 來表示抛出異常是的異常變量。

例如,這段代碼:

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

把m代表的方法編譯之後,就成了下面這樣:

try {
    // 原本的代碼
}
catch (java.io.IOException e) {
    System.out.println(e);
    throw e;
}
           

注意,插入的代碼以throw或return語句結尾。

4.2 修改方法體

CtMethod和CtConstructor提供了setBody() 方法,該方法用于取代整個方法體。它們會把你提供的源碼編譯成位元組碼,然後完全替代之前方法的方法體。如果你傳遞的源碼參數為null,那麼被替換的方法體隻會包含一條return語句。

在setBody() 方法傳遞的源碼中,以$開頭的辨別符會有一些特殊含義(這個跟上面是一樣的):

辨別符 含義 英語含義
$0, $1, $2, … this 和實參 this and actual parameters
$args 參數數組。$args 的類型是 Object[] An array of parameters. The type of $args is Object[].
$$ 所有實參,例如m($$)等同于m($1,$2,…) All actual parameters.For example, m($$) is equivalent to m($1,$2,…)
$cflow(…) cflow變量 cflow variable
$r 傳回值類型。用于強制類型轉換表達式 The result type. It is used in a cast expression.
$w 包裝類型。用于強制類型轉換表達式 The wrapper type. It is used in a cast expression.
$sig java.lang.Class對象的數組,表示參數類型 An array of java.lang.Class objects representing the formal parameter types.
$type java.lang.Class對象的數組,表示結果類型 A java.lang.Class object representing the formal result type.
$class java.lang.Class對象的數組,表示目前被編輯的類 A java.lang.Class object representing the class currently edited.
注意,這裡不能用 $_ 。

4.2.1 修改現有的表達式

Javassist允許隻修改方法體中的某一個表達式。javassist.expr.ExprEditor類用于替換方法體中的某一個表達式。使用者可以定義ExprEditor的子類來說明表達式應該如何被修改。

使用ExprEditor對象,使用者需要調用CtMethod或CtClass中的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($$); }");
        }
    });
           

該功能為,搜尋方法體中,所有對Point類的move() 方法的調用,都将其替換為如下代碼塊:

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

是以,move() 的第一個參數總是0。注意,被替換的代碼不是表達式,而是一個語句或代碼塊,并且不能包含try-catch。

instrument() 方法會搜尋方法體。如果它找到了像是“方法調用、屬性通路、對象建立”的表達式,那麼它就會調用ExprEditor對象的edit() 方法。edit() 的參數就代表了被找到的那個表達式。edit() 方法可以通過該對象來檢查和替換表達式。

調用MethodCall對象m的replace方法來将其替換為一個語句或代碼塊。如果給了的是 {},那麼該表達式就會從方法體中移除。如果你想在該表達式的前後加一些邏輯,你可以這樣寫:

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

不管是方法調用,屬性通路還是對象建立或者是其他,第二條語句都可以是:

$_ = $proceed();
           

如果表達式是個讀通路,或者:

$proceed($$);
           

如果表達式是個寫通路。

如果使用 -g 選項編譯源碼,那麼在replace() 中也是可以直接使用局部變量的(前提是class檔案中包含那個局部變量)。

4.2.2 javassist.expr.MethodCall

MethodCall對象代表一個方法的調用。它的replace() 方法會把方法調用替換成另一個語句或代碼塊。它接受一個源碼文本來代表要替換的代碼,文本中以$開頭的表示符具有特殊的含義,就跟insertBefore() 的差不多。

辨別符 含義 英語含義
$0

方法調用的目标對象。

它不等于this,它是調用方的this對象。

如果是靜态方法,$0 是null.

The target object of the method call.

This is not equivalent to this, which represents the caller-side this object.

$0 is null if the method is static.

$1, $2, … 方法調用的參數 The parameters of the method call.
$_ 方法調用的傳回值 The resulting value of the method call.
$r 方法調用的傳回值類型 The result type of the method call.
$class java.lang.Class對象,表示聲明該方法的類 A java.lang.Class object representing the class declaring the method.
$sig java.lang.Class對象的數組,表示參數類型 An array of java.lang.Class objects representing the formal parameter types.
$type java.lang.Class對象的數組,表示結果類型 A java.lang.Class object representing the formal result type.
$proceed 表達式中原始方法的名稱 The name of the method originally called in the expression.

上面的“方法調用”的意思就是MethodCall代表的那個對象。

其他的辨別符,像 w ∗ ∗ , ∗ ∗ w**,** w∗∗,∗∗args,$$,也是可以用的。

除非傳回類型是 void,否則,代碼文本中你必須要給** ∗ ∗ 賦 值 , 而 且 類 型 要 對 的 上 。 如 果 返 回 類 型 是 ∗ ∗ v o i d ∗ ∗ , 那 麼 ∗ ∗ _** 指派,而且類型要對的上。如果傳回類型是**void**,那麼** ∗​∗指派,而且類型要對的上。如果傳回類型是∗∗void∗∗,那麼∗∗_** 的類型是**Object,你也不用給他指派。

$proceed不是一個 String,而是一個特殊的文法。它後面必須跟一個被括号 () 包圍的參數清單。

4.2.3 javassist.expr.ConstructorCall

ConstructorCall對象代表一個構造函數的調用,像this() ,并且super() 包含在該構造方法體中。ConstructorCall的replace() 方法可以将一句語句或代碼塊替換掉原本的構造方法體。它接受源碼文本代表要替換的代碼,它之中的以$開頭的辨別符具有一些特殊含義,就行insertBefore的那樣:

辨別符 含義 英語含義
$0 構造方法調用的目标對象。它就等于this The target object of the constructor call. This is equivalent to this.
$1, $2, … 構造方法調用的參數 The parameters of the constructor call.
$class java.lang.Class對象,表示聲明該構造函數的類 A java.lang.Class object representing the class declaring the constructor.
$sig java.lang.Class對象的數組,表示參數類型 An array of java.lang.Class objects representing the formal parameter types.
$proceed 表達式中原始方法的名稱 The name of the method originally called in the expression.

這裡,“構造函數調用”的意思就是ConstructorCall對象代表的那個方法。

** w , w, w,args,$$**等辨別符也是可以用的

因為任何構造函數都要調用它的父類構造函數或是自己其他的構造函數,是以被替換的語句要包含一個構造函數的調用,通用使用**$proceed()**.

$proceed不是一個String,而是一個特殊的文法。它後面必須跟一個被括号 () 包圍的參數清單。

4.2.4 javassist.expr.FieldAccess

FieldAccess對象代表屬性通路。如果找到了屬性通路,那麼ExprEditor的edit() 就會接收到。FieldAccesss的replace() 方法接受一個源碼文本,用于替換原本屬性通路的代碼。

在源碼文本中,以$的辨別符具有一些特殊的含義:

辨別符 含義 英語含義
$0

包含該變量的那個對象。

它不等與this,this是通路該變量的那個方法對應的類對象。

如果變量為靜态變量,$0為null

The object containing the field accessed by the expression. This is not equivalent to this.

this represents the object that the method including the expression is invoked on.

$0 is null if the field is static.

$1

如果表達式是寫通路,那麼它代表将要被寫入的值。

否則**$1**不可用

The value that would be stored in the field if the expression is write access.

Otherwise, $1 is not available.

$_

如果表達式是讀通路,它代表讀取到的值。

否則,存儲在**$_的值将丢失。

The resulting value of the field access if the expression is read access.

Otherwise, the value stored in $_ is discarded.

$r

如果表達式是讀通路,它代表變量的類型。

否則,$r是void

The type of the field if the expression is read access.

Otherwise, $r is void.

$class java.lang.Class對象,表示聲明該屬性的類 A java.lang.Class object representing the class declaring the field.
$type java.lang.Class對象的數組,表示該變量的 A java.lang.Class object representing the field type.
$proceed 表達式中原始方法的名稱 The name of a virtual method executing the original field access. .

** w , w, w,args,$$**等辨別符也是可以用的。

如果表達式是讀通路,必須要在源碼文本中給**$_**指派,而且類型要對的上。

4.2.5 javassist.expr.NewExpr

NewExpr對象表示使用new關鍵字建立新對象(不包括數組建立)。如果找到了對象時,ExprEditor的edit() 方法就會被執行。可以使用NewExpr的replace() 方法來替換原本的代碼。

在源代碼文本中,以$開頭的辨別符具有特殊含義:

辨別符 含義 英語含義
$0 null null
$1,$2,… 構造器的參數 The parameters to the constructor.
$_ 建立對象的結果值。新建立的對象必須存儲到這個變量中。

The resulting value of the object creation.

A newly created object must be stored in this variable.

$r 建立對象的類型 The type of the created object.
$sig java.lang.Class對象的數組,表示參數類型 An array of java.lang.Class objects representing the formal parameter types.
$type java.lang.Class對象,表示建立對象的那個類的類型 java.lang.Class object representing the class of the created object.
$proceed 表達式中原始方法的名稱 The name of a virtual method executing the original object creation. .

** w , w, w,args,$$**等辨別符也是可以用的。

4.2.6 javassist.expr.NewArray

NewArray代表new關鍵字建立數組。如果找到了數組的建立, ExprEditor的edit() 方法就會被執行。NewArray的replace() 方法能夠替換建立數組的代碼。

在源代碼文本裡,以$開頭的辨別符有以下特殊含義:

辨別符 含義 英語含義
$0 null null
$1,$2,… 每個次元的大小 The size of each dimension.
$_ 數組建立的傳回值。新建立的數組必須要存到該變量中

The resulting value of the array creation.

A newly created array must be stored in this variable.

$r 被建立的數組的類型 The type of the created array.
$sig java.lang.Class對象的數組,表示參數類型 An array of java.lang.Class objects representing the formal parameter types.
$type java.lang.Class對象,表示被建立的數組的類 A java.lang.Class object representing the class of the created array.
$proceed 表達式中原始方法的名稱 The name of a virtual method executing the original array creation.

** w , w, w,args,$$**等辨別符也是可以用的。

例如,如果按照下面的方式建立數組:

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

那麼**$1和$2的值分别是3和4**,$3不可用。

如果數組是按照下面的方式建立的:

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

$1的值是3,$2不可用。

4.2.7 javassist.expr.Instanceof

Instanceof對象代表了一個instanceof語句。如果instanceof語句被發現,ExprEditor的edit() 就會被執行。Instanceof的replace() 方法會替換它原本的代碼。

在源代碼文本中,以$開頭的辨別符具有特殊的含義:

辨別符 含義 英語含義
$0 null null
$1 instanceof操作符左邊變量的值 The value on the left hand side of the original instanceof operator.
$_ 表達式的結果值。$_ 的類型為boolean The resulting value of the expression. The type of $_ is boolean.
$r instanceof操作符右邊的類型 The type on the right hand side of the instanceof operator.
$type java.lang.Class對象,表示instanceof操作符右邊的類型 A java.lang.Class object representing the type on the right hand side of the instanceof operator.
$proceed

表達式中原始方法的名稱。

它接受一個參數(類型為java.lang.Object)。

如果類型對的上,傳回true,否則為false

The name of a virtual method executing the original instanceof expression.

It takes one parameter (the type is java.lang.Object) and returns true

if the parameter value is an instance of the type on the right hand side of

the original instanceof operator. Otherwise, it returns false.

** w , w, w,args,$$**等辨別符也是可以用的。

4.2.8 javassist.expr.Cast

Cast對象代表一個強制類型轉換表達式。如果找到了強制類型轉換的表達式,ExprEditor的edit() 方法将會被執行。Cast的replace() 方法會替換原來的代碼。

在源代碼文本裡,以$開頭的辨別符有以下特殊含義:

辨別符 含義 英語含義
$0 null null
$1 被類型轉換的那個變量的值 The value the type of which is explicitly cast.
$_ 表達式結果的值。$_的類型是被強制轉換後的類型,就是() 包起來的那個。

The resulting value of the expression. The type of $_ is the same as the type

after the explicit casting, that is, the type surrounded by ().

$r 被強制轉換後的類型,或者說是被 () 包起來的那個類型。 the type after the explicit casting, or the type surrounded by () .
$type java.lang.Class對象,表示與**$r**相同的那個類型 A java.lang.Class object representing the same type as $r.
$proceed

表達式中原始方法的名稱。

他接受一個java.lang.Object類型的參數,并在強制轉換成功後傳回它。

The name of a virtual method executing the original type casting.

It takes one parameter of the type java.lang.Object and returns it after

the explicit type casting specified by the original expression.

w , w, w,args,$$ 等辨別符也是可以用的。

4.2.9 javassist.expr.Handler

Handler對象代表了try-catch語句中的catch語句。如果找到了catch語句,edit() 方法就會被執行。Handler的insertBefore() 可以在catch語句的最開始插入代碼。

在源代碼文本中,以$開頭的辨別符具有特殊的含義:

辨別符 含義 英語含義
$1 catch語句捕獲的異常對象 The exception object caught by the catch clause.
$r 捕獲異常的異常類型。用于強制類型轉換 the type of the exception caught by the catch clause. It is used in a cast expression.
$w 包裝類型,用于強制類型轉換 The wrapper type. It is used in a cast expression.
$type java.lang.Class對象,表示catch捕獲的異常對象的類型

A java.lang.Class object representing

the type of the exception caught by the catch clause.

如果給 $1 賦了新的異常對象,它會将其作為捕獲異常傳遞給原始的 catch 語句。

4.3 增加新方法或新屬性

4.3.1 增加新方法

Javassist允許使用者從零開始建立一個新的方法和構造函數。CtNewMethod和CtNewConstructor提供了幾個工廠方法,它們都是靜态方法用于建立CtMethod或CtConstructor對象。尤其是make() 方法,它可以直接傳遞源代碼,用于建立CtMethod和CtConstructor對象。

例如這個程式:

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

該代碼給Point類增添了一個public方法xmove() 。其中x是Point類原本就有的一個int屬性。

傳遞給make() 的代碼也可以包含以 $ 開頭的辨別符,就跟setBody() 方法是一樣的,除了** ∗ ∗ 之 外 。 如 果 你 還 把 ∗ ∗ m a k e ( ) ∗ ∗ 傳 遞 了 目 标 對 象 和 目 标 方 法 , 你 也 可 以 使 用 ∗ ∗ _** 之外。如果你還把**make()** 傳遞了目标對象和目标方法,你也可以使用** ∗​∗之外。如果你還把∗∗make()∗∗傳遞了目标對象和目标方法,你也可以使用∗∗proceed**。 例如:

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

這個程式建立的ymove() 的定義如下:

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

這裡面this.move替換了**$proceed**。

Javassist還提供了一些其他方法用于建立新方法。你可以先建立一個抽象方法,之後再給它一個方法體:

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);
           

你給class增添過抽象方法之後,Javassist就會把這個類變成抽象類,是以在你調用setBody() 方法後,需要顯式的把該class改變成非抽象類。

4.3.2 互相遞歸方法

如果一個類沒有增添某一個方法,那麼Javassist是不允許調用它的。(但是Javassist編譯自己調用自己的遞歸方法)。要給一個類增添互相遞歸的方法,你需要先增添一個抽象方法。假定你向增添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);
           

你必須首先把它們弄成兩個抽象方法,然後增添到class中。然後你就能給他們增加方法體,方法體中也可以進行互相調用。最後你必須把類改成非抽象類,因為你addMethod() 的時候,javassist自動把該類改成了抽象類。

4.3.3 增添屬性

Javassist也允許使用者建立一個新的屬性:

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

這個程式給Point類增添一個名為z的屬性。

如果增添的屬性需要進行值初始化,則上面的程式就要改成這樣:

CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f, "0");    // 初始化的值是0.
           

Now,addField() 方法接受了第二個參數,它代表了計算初始值表達式的源碼文本。該源碼文本可以是任何Java表達式,前提是表達式的結果類型和屬性類型比對。注意,表達式不以分号(;)結尾,意思就是0後面不用跟分号。

除此之外,上面的代碼也可用下面這個簡單的代碼代替:

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

4.3.4 删除屬性

要删除屬性或方法,可以調用CtClass中的removeField() 或 removeMethod()。也可以調用removeConstructor() 删除構造函數。

4.4 注解

CtClass, CtMethod, CtField, CtConstructor 提供了一個很友善的方法getAnnotations() 來讀取注解。它傳回一個注解類型對象。

例如,假定下列注解:

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

這個注解被這樣使用:

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

那麼,這個注解的值可以通過getAnnotations() 方法擷取。它傳回一個包含了注解類型對象的數組:

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 一個注解,是以 all 數組的長度是1,all[0] 是Author對象。該注解的屬性值可以使用Author對象的name() 和 year() 方法擷取。

要使用getAnnotation(), 目前class路徑下必須要包含注解類型,像Author。它們也必須在ClassPool對象中可通路。如果注解類型的Class檔案沒有找到,Javassist就不能擷取該注解類型成員的預設值。

4.5 運作時類支援

大多數情況下,由Javassist修改的類不需要Javassist去運作。然而,有些Javassist編譯器生成的位元組碼需要運作時類支援,那些都在javassist.runtime包中(詳細内容請參考該包的API文檔)。注意,javassist.runtime包隻負責管Javassist修改的類的運作時支援。其他Javassist修改後的類不會在運作時使用。

4.6 Import

源碼中所有的類名都必須是完全限定的(它們必須導入包名)。然而,java.lang包時一個特例;例如,Javassist編譯器可以解析Object也可以解析java.lang.Object.

為了告知編譯器當解析類時搜尋其他的包,可以調用ClassPool的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);
           

第二行告訴編譯器要導入java.awt包。是以,第三行不會抛出異常。編譯器會把Point看作java.awt.Point.

注意,importPackage() 不會影響ClassPool的get() 方法。隻有編譯器會任務導入了包。get() 方法的參數必須總是全限定名。

4.7 局限性

在目前實作中,Javassist的編譯器存在幾個局限性。這些局限性有:

  • J2SE 5.0中提到的文法(包括枚舉和泛型)還沒有得到支援。注解由Javassist的底層API支援。參見javassist.bytecode.annotation包(和getAnnotations() 以及CtBehavior )。泛型也隻是部分支援。後面的章節有詳細介紹。
  • 數組初始化的時候,以逗号分割,大括号 {} 包圍的初始化方式還不支援。除非數組的長度時1.
  • 不支援内部類和匿名類。注意,這隻是編譯器的局限。它不能編譯包含在匿名類定義中的源碼。Javassist可以讀取并修改内部/匿名類的類檔案。
  • 不支援continue和break關鍵字。
  • 編譯器不能正确的實作Java方法的多态。如果方法在一個類中具有相同的名字,但是卻有不同的參數清單,編譯器可能會出錯。例如:

    class A {}

    class B extends A {}

    class C extends B {}

    class X {

    void foo(A a) { … }

    void foo(B b) { … }

    }

如果被編譯的表達式是x.foo(new C()), x是X的一個執行個體,編譯器可能會生成一個對foo(A) 的調用,雖然編譯器可以正确的編譯foo((B)new C()).

  • 建議使用者使用 # 作為類名與靜态方法或屬性之間的分給。例如,在Java中:

    javassist.CtClass.intType.getName()

在javassist.CtClass中的靜态字段intType訓示的對象上調用getName()方法。在Javassist中,使用者可以寫上面的表達式,但是還是建議按照下面這樣寫:

javassist.CtClass#intType.getName()
           

這樣,編譯器就可以很快的解析這個表達式。

五、位元組碼API

由于我沒有位元組碼知識基礎,是以本章的翻譯可能會有很多不準的地方。

簡介

Javassist也提供了底層API用于直接編輯class檔案。要使用了該API,你需要Java位元組碼和class檔案格式的詳細知識,這樣你就可以利用API對class檔案想怎麼改就怎麼改。

如果你隻想生成一個簡單的class檔案,你可以使用javassist.bytecode.ClassFileWriter。它比javassist.bytecode.ClassFile快的多,雖然它的API最小。

5.1 擷取 ClassFile 對象

一個javassist.bytecode.ClassFile對象代表一個Class檔案。可以使用CtClass中的getClassFile() 擷取該對象。

除此之外,你也可以用根據一個Class檔案直接構造該javassist.bytecode.ClassFile對象。例如:

BufferedInputStream fin
    = new BufferedInputStream(new FileInputStream("Point.class"));
ClassFile cf = new ClassFile(new DataInputStream(fin));
           

該代碼片段建立了一個來自Point.class的ClassFile對象。

你也可以從零開始建立一個新檔案。例如:

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")));
           

該代碼生成了一個class檔案Foo.class,它包含了以下實作:

package test;
class Foo implements Cloneable {
    public int width;
}
           

5.2 增添或删除成員

ClassFile提供了addField() 和addMethod() ,用于增添屬性或方法(注意在位元組碼中,構造函數被視為一個方法)。它也提供了addAttribute() 用于增添一個屬性到class檔案中。

注意,FiledInfo, MethodInfo 和 AttributeInfo 對象包含了對ConstPool(常量池表)對象的引用。ConstPool對象必須是ClassFile對象和被增添到ClassFile對象的FieldInfo(或MethodInfo等)的公共對象。換句話說,一個FieldInfo(或MethodInfo等)對象不能在不同的ClassFile對象之間共享。

要從ClassFile對象中移除一個屬性或方法,你必須先擷取該類所有屬性的java.util.List,可以使用getField() 和getMethod() ,它們都傳回list。屬性和方法都可以使用該List對象的remove() 方法進行移除。一個屬性(Attribute)可以通過相同的方法進行移除。調用FieldInfo或MethodInfo的getAttribute() 來擷取屬性清單,然後從傳回的list中移除它。

5.3 周遊方法體

要檢查方法體中的每個位元組碼指令,CodeIterator是很有用的。要擷取這個對象,可以這樣做:

ClassFile cf = ... ;
MethodInfo minfo = cf.getMethod("move");    // we assume move is not overloaded.
CodeAttribute ca = minfo.getCodeAttribute();
CodeIterator i = ca.iterator();
           

CodeIterator對象可以讓你從開頭到結尾一行一行的通路每一個位元組碼指令。下面是CodeIterator一部分的方法API:

  • void begin() : 移動到第一個指令
  • void move(int index):移動到指定index位置的指令
  • boolean hasNext():如果還有指令,則傳回true
  • int next():傳回下一個指令的index。注意,他不會傳回下一個指令的位元組碼。
  • int byteAt(int index): 傳回該位置的無符号8bit(unsigned 8bit)值
  • int u16bitAt(int index): 傳回該位置的無符号16bit(unsigned 16bit)值。
  • int write(byte[] code, int index): 在該位置寫byte數組。
  • void insert(int index, byte[] code),在該位置插入byte數組。分支偏移量等會自動調節。
這裡我不是很會翻譯,可以直接看原版
  • void begin()

    Move to the first instruction.

  • void move(int index)

    Move to the instruction specified by the given index.

  • boolean hasNext()

    Returns true if there is more instructions.

  • int next()

    Returns the index of the next instruction.

    Note that it does not return the opcode of the next instruction.

  • int byteAt(int index)

    Returns the unsigned 8bit value at the index.

  • int u16bitAt(int index)

    Returns the unsigned 16bit value at the index.

  • int write(byte[] code, int index)

    Writes a byte array at the index.

  • void insert(int index, byte[] code)

    Inserts a byte array at the index. Branch offsets etc. are automatically adjusted.

下面這段代碼基本包含了上面所介紹的所有API:

CodeIterator ci = ... ;
while (ci.hasNext()) {
    int index = ci.next();
    int op = ci.byteAt(index);
    System.out.println(Mnemonic.OPCODE[op]);
}
           

5.4 生成位元組碼序列

Bytecode對象代表一串位元組碼指令。它是一個可增長的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() 方法擷取包含該序列的byte數組。擷取到的數組可以插入到其他的代碼屬性中。

Bytecode提供了一些方法來增添特定的指令到位元組碼序列中。它提供了addOpcode() 用于增添8bit操作碼,也提供了addIndex() 方法用于增添一個索引。每個操作碼的8bit值都被定義在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指定的class中。Bytecode對象首先被轉換成了CodeAttribute對象,然後增添到了minfo指定的方法中。該方法最終被增添到了cf類檔案中。

5.5 注解(Meta tags)

注解作為運作時不可見(或可見)的注解屬性被存儲在class檔案中。它們的屬性可以通過ClassFile,MethodInfo或FieldInfo對象擷取,調用那些對象的getAttribute(AnnotationsAttribute.invisibleTag) 方法。 更詳細的内容參見javassist.bytecode.AnnotationsAttribute 和javassist.bytecode.annotation包的javadoc手冊。

Javassist也讓你通過頂層API通路注解。如果你想通過CtClass通路注解,可以調用getAnnotations() 方法。

六、泛型

Javassist的底層API完全支援了Java5中的泛型。另一方面,頂層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中的編譯器不支援泛型,是以對于使用Javassis插入的代碼,你必須插入一個顯式的類型轉換。例如通過 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 ,是以在調用方需要顯示的增加類型轉換。例如,如果類型參數T是String, 那麼 (String) 必須像下面這樣被插入:

Wrapper w = ...
String s = (String)w.get();
           

如果編譯器是正常的Java編譯器,那麼不需要顯式的指定類型轉換,它會自動插入類型轉換代碼。

七、可變參數(int… args)

目前,Javassist不直接支援可變參數。是以要讓一個方法擁有可變參數,你必須顯式的設定方法修飾符。但是這是容易的。假定現在你想建立下面的這個方法:

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 });
           

不能使用Java原生的調用方式:

length(1, 2, 3);
           

八、J2ME

如果你要修改的檔案是J2ME環境的,那麼你必須執行預校驗。預校驗是生成堆棧映射(stack map)的基礎,它與JDK1.6中的堆棧映射表很像。隻有javassist.bytecode.MethodInfo.doPreverify為true的時候,Javassist才會為J2ME維護堆棧映射。

你也可以手工的為修改的方法生成一個堆棧映射。比如下面這個,m 是一個 CtMethod 對象,你可以調用下面方法來生成一個堆棧映射:

m.getMethodInfo().rebuildStackMapForME(cpool);
           

這裡, cpool 是一個 ClassPool 對象, 可以通過調用CtClass的 getClassPool() 方法擷取。ClassPool 對象負責從給定路徑找到class檔案,這個前面章節已經說過了。要擷取所有的CtMethod對象,可以調用CtClass的getDeclaredMethods方法。

九、拆箱和裝箱

Java中的拆箱和裝箱是個文法糖。是沒有位元組碼的。是以Javassist的編譯器不支援它們。例如,下面這個語句在Java中是合法的:

Integer i = 3;
           

因為裝箱是暗中執行。 對于Javassist來說,然而,你必須顯式的将int轉換為Integer:

Integer i = new Integer(3);
           

十、Debug

把 CtClass.debugDump 的值設定成一個目錄,那麼Javassist修改和生成的所有class檔案都将會被儲存在該目錄下。要是不想弄,把 CtClass.debugDump 設定為null就行了。預設值也是null。

例如:

CtClass.debugDump = "./dump";
           

Javassist修改的所有class檔案都将存儲在 ./dump 目錄下。