Javassist中文技術文檔

本文譯自Getting Started with Javassist,如果謬誤之處,還請指出。

  1. bytecode讀寫

  2. ClassPool

  3. Class loader

  4. 自有和定製

  5. Bytecode操控接口

  6. Generics

  7. Varargs

  8. J2ME

  9. 裝箱和拆箱

  10. 調試

     

     

     

     

    1. bytecode讀寫

Javassist是用來處理java字節碼的類庫, java字節碼一般存放在後綴名稱爲class的二進制文件中。每個二進制文件都包含一個java類或者是java接口。

Javasist.CtClass是對類文件的抽象,處於編譯中的此對象可以用來處理類文件。下面的代碼用來展示一下其簡單用法:

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();

這段程序首先獲取ClassPool的實例,它主要用來修改字節碼的,裏面存儲着基於二進制文件構建的CtClass對象,它能夠按需創建出CtClass對象並提供給後續處理流程使用。當需要進行類修改操作的時候,用戶需要通過ClassPool實例的.get()方法,獲取CtClass對象。從上面代碼中我們可以看出,ClassPool的getDefault()方法將會查找系統默認的路徑來搜索test.Rectable對象,然後將獲取到的CtClass對象賦值給cc變量。

從易於擴展使用的角度來說,ClassPool是由裝載了很多CtClass對象的HashTable組成。其中,類名爲key,CtClass對象爲Value,這樣就可以通過搜索HashTable的Key來找到相關的CtClass對象了。如果對象沒有被找到,那麼get()方法就會創建出一個默認的CtClass對象,然後放入到HashTable中,同時將當前創建的對象返回。

從ClassPool中獲取的CtClass對象,是可以被修改的。從上面的 代碼中,我們可以看到,原先的父類,由test.Rectangle被改成了test.Point。這種更改可以通過調用CtClass().writeFile()將其持久化到文件中。同時,Javassist還提供了toBytecode()方法來直接獲取修改的字節碼:

byte[] b = cc.toBytecode();

你可以通過如下代碼直接加載CtClass:

Class clazz = cc.toClass();

toClass()方法被調用,將會使得當前線程中的context class loader加載此CtClass類,然後生成 java.lang.Class對象。更多的細節 ,請參見this section below.

新建類

新建一個類,可以使用ClassPool.makeClass()方法來實現:

         ClassPool pool = ClassPool.getDefault();

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

上面的代碼展示的是創建無成員方法的Point類,如果需要附帶方法的話,我們可以用CtNewMethod附帶的工廠方法創建,然後利用CtClass.addMethod()將其追加就可以了 。

makeClass()不能用於創建新的接口。但是makeInterface()可以。接口的方法可以用CtNewmethod.abstractMethod()方法來創建,需要注意的是,在這裏,一個接口方法其實是一個abstract方法。

凍結類

如果CtClass對象被writeFile(),toClass()或者toBytecode()轉換成了類對象,Javassist將會凍結此CtClass對象。任何對此對象的後續更改都是不允許的。之所以這樣做,主要是因爲此類已經被JVM加載,由於JVM本身不支持類的重複加載操作,所以不允許更改。

一個凍結的CtClass對象,可以通過如下的代碼進行解凍,如果想更改類的話,代碼如下:

 

CtClasss cc = ...;
    :
cc.writeFile();
cc.defrost();
cc.setSuperclass(...);    // OK since the class is not frozen.

調用了defrost()方法之後,CtClass對象就可以隨意修改了。

如果ClassPool.doPruning被設置爲true,那麼Javassist將會把已凍結的CtClass對象中的數據結構進行精簡,此舉主要是爲了防止過多的內存消耗。而精簡掉的部分,都是一些不必要的屬性(attriute_info結構)。因此,當一個CtClass對象被精簡之後,方法是無法被訪問和調用的,但是方法名稱,簽名,註解可以被訪問。被精簡過的CtClass對象可以被再次解凍。需要注意的是,ClassPool.doPruning的默認值爲false。

爲了防止CtClass類被無端的精簡,需要優先調用stopPruning()方法來進行阻止:

CtClasss cc = ...;
cc.stopPruning(true);
    :
cc.writeFile(); //轉換爲類文件.
                //cc不會被精簡.

這樣,CtClass對象就不會被精簡了。當writeFile()方法調用之後,我們就可以進行解凍,然後爲所欲爲了。

需要注意的是:在調試的時候, debugWriteFile()方法可以很方便的防止CtClass對象精簡和凍住。

類搜索路徑

ClassPool.getDefault()方法的搜索路徑和JVM的搜索路徑是一致的。如果程序運行在JBoss或者Tomcat服務器上,那麼ClassPool對象也許不能夠找到用戶類,原因是應用服務器用的是多個class loader,其中包括系統的class loader來加載對象。正因如此,ClassPool需要 附加特定的類路徑才行。 假設如下的pool實例代表ClassPool對象:

pool.insertClassPath(new ClassClassPath(this.getClass()));

上面的代碼段註冊了this所指向的類路徑下面的類對象。你可以用其他的類對象來代替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包下面的類。比如加載org.javassist.test.Main類,此類將會從如下路徑獲取:

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

此外,你甚至可以直接使用一串字節碼,然後創建出CtClass對象。示例如下:

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

從上面代碼可以看出,ClassPool加載了ByteArrayClasPath構建的對象,然後利用get()方法並通過類名,將對象賦值給了CtClass對象。

如果你不知道類的全名,你也可以用makeClass()來實現:

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

makeClass()方法利用給定的輸入流構建出CtClass對象。你可以用餓漢方式直接創建出ClassPool對象,這樣當搜索路徑中有大點的jar文件需要加載的時候,可以提升一些性能,之所以 這樣做,原因是ClassPool對象按需加載類文件,所以它可能會重複搜索整個jar包中的每個類文件,正因爲如此,makeClass()可以用於優化查找的性能。被makeClass()方法加載過的CtClass對象將會留存於ClassPool對象中,不會再進行讀取。

用戶可以擴展類搜索路徑。可以通過定義一個新的類,擴展自ClassPath接口,然後返回一個insertClassPath即可。這種做法可以允許其他資源被包含到搜索路徑中。

 

2. ClassPool

一個ClassPool裏面包含了諸多的CtClass對象。每當一個CtClass對象被創建的時候,都會在ClassPool中做記錄。之所以這樣做,是因爲編譯器後續的源碼編譯操作可能會通過此類關聯的CtClass來獲取。

比如,一個代表了Point類的CtClass對象,新加一個getter()方法。之後,程序將會嘗試編譯包含了getter()方法的Point類,然後將編譯好的getter()方法體,添加到另外一個Line類上面。如果CtClass對象代表的Point類不存在的話,那麼編譯器就不會成功的編譯getter()方法。需要注意的是原來的類定義中並不包含getter()方法 。因此,要想正確的編譯此方法,ClassPool對象必須包含程序運行時候的所有的CtClass對象。

避免內存溢出

CtClass對象非常多的時候,ClassPool將會消耗內存巨大。爲了避免個問題,你可以移除掉一些不需要的CtClass對象。你可以通過調用CtClass.detach()方法來實現,那樣的話此CtClass對象將會從ClassPool移除。代碼如下:

CtClass cc = ... ;
cc.writeFile();
cc.detach();

此CtClass對象被移除後,不能再調用其任何方法。但是你可以調用ClassPool.get()方法來創建一個新的CtClass實例。

另一個方法就是用新的ClassPool對象來替代舊的ClassPool對象。如果舊的ClassPool對象被垃圾回收了,那麼其內部的CtClass對象也都會被垃圾回收掉。下面的代碼可以用來創建一個新的ClassPool對象:

ClassPool cp = new ClassPool(true);
//如果需要的話,利用appendClassPath()來添加額外的搜索路徑

上面的代碼和ClassPool.getDefault()來創建ClassPool,效果是一樣的。需要注意的是,ClasssPool.getDefault()是一個單例工廠方法,它能夠創建出一個唯一的ClassPool對象並進行重複利用。new ClassPool(true)是一個很快捷的構造方法,它能夠創建一個ClassPool對象然後追加系統搜索路徑到其中。和如下的代碼創建行爲表現一致:

ClassPool cp = new ClassPool();
cp.appendSystemPath();  // or append another path by appendClassPath()

級聯ClassPools

如果應用運行在JBOSS/Tomcat上, 那麼創建多個ClassPool對象將會很有必要。因爲每個類加載其都將會持有一個ClassPool的實例。應用此時最好不用getDefault()方法來創建ClassPool對象,而是使用構造來創建。

多個ClassPool對象像java.lang.ClassLoader一樣做級聯,代碼如下:

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

如果child.get()被調用,子ClassPool將會首先從父ClassPool進行查找。當父ClassPool查找不到後,然後將會嘗試從./classes目錄進行查找。

如果child.childFirstLookup = true, 子ClassPool將會首先查找自己的目錄,然後查找父ClassPool,代碼如下:

ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath();         //和默認的搜索地址一致.
child.childFirstLookup = true;       //修改子類搜索行爲.

爲新類重命名

可以從已有類創建出新的類,代碼如下:

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

此代碼首先從Point類創建了CtClass對象,然後調用setName()重命名爲Pair。之後,所有對CtClass對象的引用,將會由Point變成Pair。

需要注意的是setName()方法改變ClassPool對象中的標記。從可擴展性來看,ClassPool對象是HashTable的合集,setName()方法只是改變了key和Ctclass對象的關聯。

因此,對於get("Point")方法之後的所有調用,將不會返回CtClasss對象。ClassPool對象再次讀取Point.class的時候,將會創建一個新的CtClass,這是因爲和Point關聯的CtClass對象已經不存在了,請看如下代碼:

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是不一致的.

cc1和cc2將會指向cc,但是cc3卻不會。需要注意的是,在cc.setName("Pair")執行後,cc和cc1指向的CtClass對象都變成了指向Pair類。

ClassPool對象用來維護類之間和CtClass對象之間一對一的映射關係。Javassist不允許兩個不同的CtClass對象指向同一個類,除非兩個獨立的ClassPool存在的情況下。這是爲實現程序轉換而保證其一致性的最鮮明的特點。

我們知道,可以利用ClassPool.getDefault()方法創建ClassPool的實例,代碼片段如下(之前已經展示過):

 

ClassPool cp = new ClassPool(true);

如果你有兩個ClassPool對象,那麼你可以從這兩個對象中分別取出具有相同類文件,但是隸屬於不同的CtClass對象生成的,此時可以通過修改這倆CtClass對象來生成不同的類。

 

從凍結類中創建新類

當CtClass對象通過writeFile()方法或者toBytecode()轉變成類文件的時候,Javassist將不允許對這個CtClass對象有任何修改。因此,當代表Point類的CtClass對象被轉換成了類文件,你不能夠先拷貝Point類,然後修改名稱爲Pair類,因爲Point類中的setName()方法是無法被執行的,錯誤使用示例如下:

 

ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair");    // wrong since writeFile() has been called.

爲了能夠避免這種限制,你應該使用getAndRename()方法,正確示例如下:

 

 

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

如果getAndRename()方法被調用,那麼ClassPool首先會基於Point.class來創建一個新的CtClass對象。之後,在CtClass對象被放到HashTable前,它將CtClass對象名稱從Point修改爲Pair。因此,getAndRename()方法可以在writeFile()方法或者toBytecode()方法執行後去修改CtClass對象。

 

3. 類加載器

如果預先知道需要修改什麼類,最簡單的修改方式如下:

    1. 調用ClassPool.get()方法獲取CtClass對象

    2. 修改此對象

    3. 調用CtClass對象的writeFile()方法或者toBytecode()方法來生成類文件。
如果檢測類是否修改行爲發生在程序加載的時候,那麼對於用戶說來,Javassist最好提供這種與之匹配的類加載檢測行爲。事實上,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");
        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()方法中, say()方法被插入了println()方法,之後這個被修改的Hello類實例被創建,say()方法被調用。

 

需要注意的是,上面代碼中,Hello類是放在toClass()之後被調用的,如果不這麼做的話,JVM將會先加載Hello類,而不是在toClass()方法加載Hello類之後再調用Hello類,這樣做會導致加載失敗(會拋出LinkageError錯誤)。比如,如果Test.main()方法中的代碼如下:

 

public static void main(String[] args) throws Exception {
    Hello orig = new Hello();
    ClassPool cp = ClassPool.getDefault();
    CtClass cc = cp.get("Hello");
   CtMethod m = cc.getDeclaredMethod("say");
   m.insertBefore("{ System.out.println(\"Hello.say():\"); }");
   Class c = cc.toClass();
   Hello h = (Hello)c.newInstance();
   h.say();
}

main方法中,第一行的Hello類會被加載,之後調用toClass()將會報錯,因爲一個類加載器無法在同一時刻加載兩個不同的Hello類版本。

 

如果程序跑在JBoss/Tomcat上,利用toClass()方法可能會有些問題。在這種情況下,你將會遇到ClassCastException錯誤,爲了避免這種錯誤,你必須爲toClass()方法提供非常明確的類加載器。比如,在如下代碼中,bean代表你的業務bean對象的時候:

 

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

則就不會出現上述問題。你應當爲toClass()方法提供已經加載過程序的類加載器才行。

 

toClass()的使用會帶來諸多方便,但是如果你需要更多更復雜的功能,你應當實現自己的類加載器。

3.2 java中的類加載

在java中,多個類加載器可以共存,不同的類加載器會創建自己的應用區域。不同的類加載器可以加載具有相同類名稱但是內容不盡相同的類文件。這種特性可以讓我們在一個JVM上並行運行多個應用。

需要注意的是JVM不支持動態的重新加載一個已加載的類。一旦類加載器加載了一個類,那麼這個類或者基於其修改的類,在JVM運行時,都不能再被加載。因此,你不能夠修改已經被JVM加載的類。但是,JPDA(Java Platform Debugger Architecture)支持這種做法。具體請見 Section 3.6.

如果一個類被兩個不同的類加載器加載,那麼JVM會將此類分成兩個不同的類,但是這兩個類具有相同的類名和定義。我們一般把這兩個類當做是不同的類,所以一個類不能夠被轉換成另一個類,一旦這麼做,那麼這種強轉操作將會拋出錯誤ClassCastException。

比如,下面的例子會拋錯:

 

MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj;    //會拋出ClassCastException錯誤.

Box類被兩個類加載器所加載,試想一下,假設CL類加載器加載的類包含此代碼段,由於此代碼段指向MyClassLoader,Class,Object,Box,所以CL加載器也會將這些東西加載進來(除非它是其它類加載器的代理)。因此變量b就是CL中的Box類。從另一方面說來,myLoader也加載了Box類,obj對象是Box類的實例,因此,代碼的最後一行將一直拋出ClassCastException錯誤,因爲obj和b是Box類的不同實例副本。

多個類加載器會形成樹狀結構,除了底層引導的類加載器外,每一個類加載器都有能夠正常的加載子加載器的父加載器。由於加載類的請求可以被類加載器所代理,所以一個類可能會被你所不希望看到的類加載器所加載。因此,類C可能會被你所不希望看到的類加載器所加載,也可能會被你所希望的加載器所加載。爲了區分這種現象,我們稱前一種加載器爲類C的虛擬引導器,後一種加載器爲類C的真實加載器。
此外,如果類加載器CL(此類加載器爲類C的虛擬引導器)讓其父加載器PL來加載類C,那麼相當於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(); }
}
假如Window類被L加載器所加載,那麼Window的虛擬加載器和實際加載器都是L。由於Window類中引用了Box類,JVM將會加載Box類,這裏,假設L將此加載任務代理給了其父加載器PL,那麼Box的類加載器將會變成L,但是其實際加載器將會是PL。因此,在此種情況下,Point類的虛擬加載器將不是L,而是PL,因爲它和Box的實際加載器是一樣的。因此L加載器將永遠不會加載Point類。
接下來,讓我們看一個少量更改過的例子:
public class Point {
    private int x, y;
    public int getX() { return x; }
        :
}

public class Box {      // the initiator is L but the real loader is PL
    private Point upperLeft, size;
    public Point getSize() { return size; }
        :
}

public class Window {    // loaded by a class loader L
    private Box box;
    public boolean widthIs(int w) {
        Point p = box.getSize();
        return w == p.getX();
    }
}
現在看來,Window類指向了Point,因此類加載器L要想加載Point的話,它必須代理PL。必須杜絕的情況是,兩個類加載器加載同一個類的情況。其中一個類加載器必須能夠代理另一個才行。
當Point類加載後,L沒有代理PL,那麼widthIs()將會拋出ClassCastExceptioin。由於Box類的實際加載器是PL,所以指向Box類的Point類將也會被PL所加載。因此,getSize()方法的最終結果將是被PL加載的Point對象的實例。反之,widthIs()方法中的p變量的類型將是被L所加載的Point類。對於這種情況,JVM會將其視爲不同的類型,從而因爲類型不匹配而拋出錯誤。
這種情況,雖然不方便,但是卻很有必要,來看一下如下代碼段:
Point p = box.getSize();
沒有拋出錯誤,Window將會破壞Point對象的包裝。舉個例子吧,被PL加載的Point類中,x字段是私有的。但是,如果L利用如下的定義加載了Point類的話,那麼Window類是可以直接訪問x字段的:
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提供了javassist.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類的父類,之後程序會加載這個修改的類並創建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()將事件監聽器添加到javassist.Loader對象上的時候,上面的start()方法將會被觸發。而onLoad()方法的觸發先於javassist.Loader加載一個類,因此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方法,因爲javassistLoader會調用這些方法來獲取類文件。
爲了能夠運行MyApp類中的MyTranslator對象,寫了一個主方法如下:
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是截然不同的。後者先使用父類加載器進行加載,如果父類加載器找不到類,則嘗試用當前加載器進行加載。而javassist.Load在如下情況下,則嘗試直接加載:
ClassPool對象上,無法找到get方法
或者
父類使用delegateLoadingOf()方法進行加載
Javassist可以按照搜索的順序來加載已修改的類,但是,如果它無法找到已修改的類,那麼將會由父類加載器進行加載操作。一旦當一個類被父加載器所加載,那麼指向此類的其他類,也將被此父加載器所加載,因爲,這些被加載類是不會被修改的。如果你的程序無法加載一個已修改的類,你需要確認所有的類是否是被javassist.Loader所加載。
 
3.4 打造一個類加載器
用javassist打造一個簡單的類加載器,代碼如下:
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文件夾下,需要確保當前文件夾不在類搜索目錄下,否則將會被SampleLoader的父類加載器,也就是系統默認的類加載器所加載。./class目錄名稱在insertClassPath方法中必須要有所體現,當然此目錄名稱是可以隨意改變的。接下來我們運行如下命令:
% 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類成功的被加載,然後打印出了hiddenValue。
需要注意的是:用如上的方式來修改rt.jar中的系統類並進行部署,會違反Java 2 Runtime Environment binary code license.
 

3.6 運行狀態下重新加載類

如果JVM中的JPDA(Java Platform Debugger Architecture)是可用狀態,那麼一個類是可以被動態加載的。JVM加載類後,此類的之前版本將會被卸載,而新版本將會被加載。所以,從這裏看出,在運行時狀態,類是可以被動態更改的。然而,新的類必須能夠和舊的類兼容,是因爲JVM不允許直接更改類的整體框架,他們必須有相同的方法和字段。
Javassist提供了簡單易用的方式來重新加載運行時的類。想要獲取更多內容,請翻閱javassist.tools.HotSwapper的API文檔。
 

4. 定製化

CtClass提供了很多方法來用進行定製化。Javassist可以和Java的反射API進行聯合定製。CtClass提供了getName方法,getSuperclass方法,getMethods方法等等。CtClass同時也提供了方法來修改類定義,允許添加新的字段,構造,方法等。即便對於檢測方法體這種事情來說,也是可行的。
方法都是被CtMethod對象所代表,它提供了多個方法用於改變方法的定義,需要注意的是,如果方法繼承自父類,那麼在父類中的同樣方法將也會被CtMethod所代表。CtMethod對象可以正確的代表任何方法聲明。

比如,Point類有一個move方法,其子類ColorPoint不會重寫move方法, 那麼在這裏,兩個move方法,將會被CtMethod對象正確的識別。如果CtMethod對象的方法定義被修改,那麼此修改將會反映到兩個方法上。如果你想只修改ColorPoint類中的move方法,你需要首先創建ColorPoint的副本,那麼其CtMethod對象將也會被複制,CtMethod對象可以使用CtNewMethod.copy方法來實現。

Javassist不支持移除方法或者字段,但是支持修改名字。所以如果一個方法不再需要的話,可以在CtMethod中對其進行重命名並利用setName方法和setModifiers方法將其設置爲私有方法。

Javassist不支持爲已有的方法添加額外的參數。但是可以通過爲一個新的方法創建額外的參數。比如,如果你想添加一個額外的int參數newZ到Point類的方法中:

 

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

你應當在Point類中添加如下方法:

 

 

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

Javassist同時也提供底層的API來直接修改原生的類文件。比如,CtClass類中的getClassFile方法可以返回一個ClassFile對象來代表一個原生的類文件。而CtMethod中的getMethodInfo方法則返回MethodInfo對象來代表一個類中的method_info結構。底層的API單詞大多數來自於JVM,所以用於用起來不會感覺到陌生。更多的內容,可以參看 javassist.bytecode package.

 

Javassist修改類文件的時候,一般不需要javassist.runtime包,除非一些特別的以$符號開頭的。這些特殊符號會在後面進行講解。更多的內容,可以參考javassist.runtime包中的API文檔。

 

4.1 方法體前/後穿插代碼段

CtMethod和CtConstructor提供了insertBefore,insertAfter,addCatch三個方法,它們用於在已存在的方法中插入代碼段。使用者可以插入java代碼段是因爲Javassist內置了一個簡易的java編譯器來處理這些源碼。此編譯器會將java源碼編譯成字節碼,然後插入到方法體中。
同時,在指定行號的位置插入代碼段也是允許的(只有當行號在當前類中存在)。CtMethod和CtConstructor中的insertAt方法帶有源碼輸入和行號的定義,它能夠將編譯後的代碼段插入到指定了行號的位置。
insertBefore,insertAfter,addCatch和insertAt方法均接受一個String類型的代表源碼塊的入參。此代碼段可以是簡單的控制類語句if和while,也可以是以分號結尾的表達式,都需要用左右大括號{}進行包裝。因此,下面的示例源碼都是符合要求的代碼段:
System.out.println("Hello");
{ System.out.println("Hello"); }
if (i < 0) { i = -i; }
代碼段可以指向字段和方法,也可以爲編譯器添加-g選項來讓其指向插入的方法中的參數。否則,只能利用$0,$1,$2...這種如下的變量來進行訪問。雖然不允許訪問方法中的本地變量,但是在方法體重定義一個新的本地變量是允許的。例外的是,編譯器開啓了-g選項的話,insertAt方法是允許代碼段訪問本地變量的。
insertBefore,insertAfter,addCatch和insertAt入參中的String對象,也就是用戶輸入的代碼段,會被Javassist中的編譯器編譯,由於此編譯器支持語言擴展,不同的$符號有不同的含義:
$0$1$2, ...     this 和實參
$args 參數列表. $args的類型Object[].
$$ 所有實參.例如, m($$) 等價於 m($1,$2,...)
 
$cflow(...) cflow變量
$r 結果類型. 用於表達式轉換.
$w 包裝類型. 用於表達式轉換.
$_ 結果值
$sig java.lang.Class列表,代表正式入參類型
$type java.lang.Class對象,代表正式入參值.
$class java.lang.Class對象,代表傳入的代碼段.

$0, $1, $2, ...

傳給目標方法的參數$1,$2...將會替換掉原始的參數名稱。$1代表第一個參數,$2代表第二個參數,以此類推。這些參數的類型和原始的參數類型是一致的。$0等價於this關鍵字,如果方法爲static,那麼$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();
需要注意的是,insertBefore方法中的代碼段是被大括號{}包圍的,此方法只接受一個被大括號包圍的代碼段入參。
更改之後的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...是可以被更新的。如果一個新的值被賦予了這幾個變量中的任意一個,那麼這個變量對應的參數值也會被更新。下面來說說其他的參數。

$args

$args變量代表所有參數列表。其類型爲Object數組類型。如果一個參數類型基礎數據類型,比如int,那麼將會被轉換爲java.lang.Integer並放到$args中。因此,$args[0]一般情況下等價於$1,除非第一個參數的類型爲基礎數據類型。需要注意的是,$args[0]和$0是不等價的,因爲$0代表this關鍵字。
如果object列表被賦值給$args,那麼列表中的每個元素將會被分配給對應的參數。如果一個參數的類型爲基礎數據類型,那麼對應的正確的數據類型爲包裝過的類型。此轉換會發生在參數被分配之前。

$$

$$是被逗號分隔的所有參數列表的縮寫。比如,如果move方法中的參數數量有三個,那麼
move($$)
等價於:
move($1,$2,$3)
如果move()無入參,那麼move($$)等價於move().
$$也可以被用於其他的場景,如果你寫了如下的表達式:
exMove($$,context)
那麼此表達式等價於:
exMove($1,$2,$3,context)
需要注意的是,$$雖說是方法調用的通用符號,但是一般和$proceed聯合使用,後面會講到。

$cflow

代表着“流程控制”。這個只讀變量會返回方法的遞歸調用深度。
假設如下的方法代表CtMethod中的對象cm:
int fact(int n) {
    if (n <= 1)
        return n;
    else
        return n * fact(n - 1);
}
爲了使用$cflow,首先需要引用$cflow,用於監聽fact方法的調用:
CtMethod cm = ...;
cm.useCflow("fact");
useCflow()方法就是用來聲明$cflow變量。任何可用的java命名都可以用來進行識別。此名稱也可以包含.(點號),比如"my.Test.face"也是可以的。
然後,$cflow(fact)代表着方法cm遞歸調用的深度。當方法第一次被調用的時候,$cflow(fact)的值爲0,再調用一次,此值將會變爲1.比如:
cm.insertBefore("if ($cflow(fact) == 0)"
              + "    System.out.println(\"fact \" + $1);");
代碼段將fact方法進行編譯以便於能夠看到對應的參數。由於$cflow(fact)被選中,那麼對fact方法的遞歸調用將不會顯示參數。
$cflow的值是當前線程中,從cm方法中,最上層棧幀到當前棧幀的值。$cflow同時和cm方法在同一個方法內部的訪問權限也是不一樣的。

$r

代表着結果類型,必須在轉換表達式中用作類型轉換。比如,如下用法:
Object result = ... ;
$_ = ($r)result;
如果結果類型爲基礎數據類型,那麼($r)需要遵循如下的規則:
首先,如果操作數類型是基礎數據類型,($r)將會被當做普通的轉義符。相反的,如果操作數類型是包裝類型,那麼($r)將會把此包裝類型轉換爲結果類型,比如如果結果類型是int,那麼($r)會將java.lang.Integer轉換爲intl;如果結果類型是void,那麼($r)將不會進行類型轉換;如果當前操作調用了void方法,那麼($r)將會返回null。舉個例子,如果foo方法是void方法,那麼:
$_ = ($r)foo();
是一個有效的申明。
轉換符號($r)同時也用於return申明中,即便返回類型是void,如下的return申明也是有效的:
return ($r)result;
這裏,result是一個本地變量,由於($r)這裏做了轉換,那麼返回結果是無效的。此時的return申明和沒有任何返回的return申明是等價的:
return;

$w

代表包裝類型。必須在轉義表達式中用於類型轉換。($w)將基礎類型轉換爲對應的包裝類型,如下代碼示例:
Integer i = ($w)5;
結果類型依據($w)後面的表達式來確定,如果表達式是double類型,那麼包裝類型則爲java.lang.Double。如果($w)後面的表達式不是基礎類型,那麼($w)將不進行任何轉換。

$_

CtMethod和CtConstructor中的insertAfter方法將編譯過的代碼插入到方法的尾部。之前給過的一些例子有關insertAfter的例子中,不僅包括$0.$1這種例子的講解,而且包括$_的這種例子。說道$_變量,它用來代表方法的結果值。其變量類型是方法返回的結果類型。如果返回的結果類型是void,那麼$_的類型是Object類型,但是其值爲null。
儘管利用insertAfter插入的編譯過的代碼,是在方法返回之前被執行的,但是這種代碼也可以在在方法拋出的exception中執行。爲了能夠讓其在方法拋出的exception中執行,insertAfter方法中的第二個參數asFinally必須爲true。
當exception被拋出的時候,利用insertAfter方法插入的代碼段將會和作爲finally代碼塊來執行。此時在編譯過的代碼中,$_的值爲0或者null。當此代碼段執行完畢後,exception會被重新拋給調用端。需要注意的是,$_是永遠不會被拋給調用端的,它會直接被拋棄掉。

$sig

$type的值是java.lang.Class對象,代表着返回值的正確的類型。如果它指向的是構造器,那麼其值爲Void.class。

$class

$class的值是java.lang.Class對象,代表着當前編輯的方法,此時和$0是等價的。

addCatch()

此方法用於將代碼段插入到方法體中進行執行,在執行過程中一旦方法體拋出exception,可以控制給發送給客戶端的返回。下面的源碼展示了利用特殊的變量$e來指向exception:
CtMethod m = ...;
CtClass etype = ClassPool.getDefault().get("java.io.IOException");
m.addCatch("{ System.out.println($e); throw $e; }", etype);
此方法體m被翻譯出來後,展示如下:
try {
    the original method body
}
catch (java.io.IOException e) {
    System.out.println(e);
    throw e;
}
需要注意的是,插入的代碼段必須以throw或者return命令結尾。
 

4.2 修改方法體

CtMethod和CtContructor提供setBody方法來取代整個方法體。此方法能夠將傳入的代碼段編譯爲Java字節碼,然後用此字節碼將其原有的方法體給替換掉。如果給定代碼段爲空,那麼被替換的方法體將只有return 0聲明,如果結果類型爲void,那麼則只有 return null聲明。
外部傳入給setBody方法的代碼段,會包含如下的以$開頭的識別碼,這些識別碼有不同的含義:
$0$1$2, ...     this 和實參
$args 參數列表.$args類型爲Object數組.
$$ 所有參數.
$cflow(...) cflow變量
$r 結果類型. 用於表達式轉換.
$w 包裝類型. 用於表達式轉換.
$sig java.lang.Class對象數組,代表正式的參數類型.
$type java.lang.Class對象,代表正式的結果類型.
$class java.lang.Class對象,代表當前操作的方法 (等價於$0的類型).
需要注意的是,此時$_是不可用的。

利用源文本替換現有表達式

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($$); }");
        }
    });
上面例子可以看出,通過搜索cm方法體中,通過替換掉Point類中的move方法爲如下代碼後,
{ $1 = 0; $_ = $proceed($$); }
move方法中的第一個參數將永遠爲0,需要注意的替換的代碼不僅僅是表達式,也可以是聲明或者代碼塊,但是不能是try-catch聲明。
instrument方法可以用來搜索方法體,如果找到了待替換的表達式,比如說方法體,字段,創建的類等,之後它會調用ExprEditor對象中的edit方法來進行修改。傳遞給edit方法的參數是找尋到的表達式對象,然後edit方法就可以通過此表達式對象來進行替換操作。
通過調用傳遞給edit方法的表達式對象中的replace方法,可以用來替換成給定的的表達式聲明或者代碼段。如果給定的代碼段是空的,那麼也就是說,將會執行replace("{}")方法,那麼之前的代碼段將會在方法體中被移除。如果你僅僅是想在表達式之前或者之後插入代碼段操作,那麼你需要將下面的代碼段傳遞給replace方法:
{ before-statements;
  $_ = $proceed($$);
  after-statements; }
此代碼段可以是方法調用,字段訪問,對象創建等等。
再來看看第二行聲明:
$_ = $proceed();
上面表達式代表着讀訪問操作,也可以用如下聲明來代表寫訪問操作:
$proceed($$);
目標表達式中的本地變量是可以通過replace方法傳遞到被instrument方法查找到的代碼段中的,如果編譯的時候開啓了-g選項的話。

javassist.expr.MethodCall

MethodCall對象代表了一個方法調用,它裏面的replace方法可以對方法調用進行替換,它通過接收準備傳遞給insertBefore方法中的以$開頭的識別符號來進行替換操作:
$0 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 object representing the class declaring the method.
$sig     An array of java.lang.Class objects representing the formal parameter types.
$type     java.lang.Class object representing the formal result type.
$proceed     The name of the method originally called in the expression.
這裏,方法調用是指MethodCall對象。$w,$args和$$在這裏都是可用的,除非方法調用的結果類型爲void,此時,$_必須被賦值且$_的類型就是返回類型。如果調用的結果類型爲Object,那麼$_的類型就是Object類型且賦予$_的值可以被忽略。
$proceed不是字符串,而是特殊的語法,它後面必須利用小括號()來包上參數列表。

javassist.expr.ConstructorCall

代表構造器調用,比如this()調用和構造體中的super調用。其中的replace方法可以用來替換代碼段。它通過接收insertBefore方法中傳入的含有以$開頭的代碼段來進行替換操作:
$0 The target object of the constructor call. This is equivalent to this.
$1$2, ...     The parameters of the constructor call.
$class     java.lang.Class object representing the class declaring the constructor.
$sig     An array of java.lang.Class objects representing the formal parameter types.
$proceed     The name of the constructor originally called in the expression.
這裏,構造器調用代表着ContructorCall對象,其他的符號,比如$w,$args和$$也是可用的。
由於構造器調用,要麼是父類調用,要麼是類中的其他構造器調用,所以被替換的方法體必須包含構造器調用操作,一般情況下都是調用$proceed().
$proceed不是字符串,而是特殊的語法,它後面必須利用小括號()來包上參數列表。

javassist.expr.FieldAccess

此對象代表着字段訪問。ExprEditor中的edit方法中如果有字段訪問被找到,那麼就會接收到這個對象。FieldAccess中的replace方法接收待替換的字段。
在代碼段中,以$開頭的識別碼有如下特殊的含義:
$0 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 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 The type of the field if the expression is read access. 
Otherwise, $r is void.
 
$class     java.lang.Class object representing the class declaring the field.
$type java.lang.Class object representing the field type.
$proceed     The name of a virtual method executing the original field access. .
其他的識別符,例如$w,$args和$$都是可用的。如果表達式是可訪問的,代碼段中,$_必須被賦值,且$_的類型就是此字段的類型。

javassist.expr.NewExpr

NewExpr對象代表利用new操作符來進行對象創建。其edit方法接收對象創建行爲,其replace方法則可以接收傳入的代碼段,將現有的對象創建的表達式進行替換。
在代碼段中,以$開頭的識別碼有如下含義:
$0 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     An array of java.lang.Class objects representing the formal parameter types.
$type     java.lang.Class object representing the class of the created object.
$proceed     The name of a virtual method executing the original object creation. .
其他的識別碼,比如$w,$args和$$也都是可用的。

javassist.expr.NewArray

此對象表示利用new操作符進行的數組創建操作。其edit方法接收數組創建操作的行爲,其replace方法則可以接收傳入的代碼段,將現有的數組創建的表達式進行替換。
在代碼段中,以$開頭的識別碼有如下含義:
$0 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.
$type     java.lang.Class object representing the class of the created array.
$proceed     The name of a virtual method executing the original array creation. .
其他的識別碼,比如$w,$args和$$也是可用的。
比如,如果數組創建的表達式如下:
String[][] s = new String[3][4];
那麼,$1和$2的值將分別爲3和4,而$3則是不可用的。
但是,如果數組創建的表達式如下:
String[][] s = new String[3][];
那麼,$1的值爲3,而$2是不可用的。

javassist.expr.Instanceof

此對象代表instanceof表達式。其edit方法接收instanceof表達式行爲,其replace方法則可以接收傳入的代碼段,將現有的表達式進行替換。
在代碼段中,以$開頭的識別碼有如下含義:
$0 null.
$1 The value on the left hand side of the original instanceof operator.
$_ The resulting value of the expression. The type of $_ is boolean.
$r The type on the right hand side of the instanceof operator.
$type java.lang.Class object representing the type on the right hand side of the instanceof operator.
$proceed     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,$args和$$也是可用的。

javassist.expr.Cast

此對象代表顯式類型轉換。其edit方法接收顯式類型轉換的行爲,其replace方法則可以接收傳入的代碼段,將現有的代碼段進行替換。
在代碼段中,以$開頭的識別碼有如下的含義:
$0 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 object representing the same type as $r.
$proceed     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,$args和$$也是可用的。

javassist.expr.Handler

此對象代表try-catch申明中的catch子句。其edit方法接收catch表達式行爲,其insertBefore方法將接收的代碼段進行編譯,然後將其插入到catch子句的開始部分。
在代碼段中,以$開頭的識別碼有如下的含義:
$1 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 object representing 
the type of the exception caught by the catch clause.
如果一個新的exception對象被賦值給$1,那麼它將會將此exception傳遞給原有的catch子句並被捕捉。

4.3 添加新方法或字段

添加一個方法

Javassist一開始就允許用戶創建新的方法和構造,CtNewMethod和CtNewConstructor提供了多種靜態工廠方法來創建CtMethod或者CtConstructor對象。特別說明一下,其make方法可以從給定的代碼段中創建CtMethod或者CtContructor對象。
比如,如下程序:
CtClass point = ClassPool.getDefault().get("Point");
CtMethod m = CtNewMethod.make(
                 "public int xmove(int dx) { x += dx; }",
                 point);
point.addMethod(m);
添加了一個公共方法xmove到Point類中,此例子中,x是Point類中的int字段。
make方法中的代碼段可以包含以$開頭的識別碼,但是setBydy方法中的$_除外。如果目標對象和目標方法的名字也傳遞給了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); }
需要注意的是,$proceed已經被this.move替換掉了。
Javassist也提供另一種方式來添加新方法,你可以首先創建一個abstract方法,然後賦予它方法體:
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);
如果一個abstract方法被添加到了類中,此時Javassist會將此類也變爲abstract,爲了解決這個問題,你不得不利用setBody方法將此類變回非abstract狀態。

相互遞歸調用方法

當一個方法調用另一個爲添加到操作類中的方法時,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);
首先,你需要創建兩個abstract方法並把他們添加到類中。
然後,爲方法設置方法體,方法體內部可以實現相互調用。
最後,將類變爲非abstract的,因爲addMethod添加abstract方法的時候,會自動將類變爲abstract的。

添加字段

Javassist允許用戶創建一個新的字段:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f);
上面的diam會添加z字段到Point類中。
如果添加的字段需要設定初始值的話,代碼需要被改爲如下方式來進行:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = new CtField(CtClass.intType, "z", point);
point.addField(f, "0");    // initial value is 0.
現在,addField方法接收了第二個用於計算初始值的參數。此參數可以爲任何符合要求的java表達式。需要注意的是,此表達式不能夠以分號結束(;)。
此外,上面的代碼可以被重寫爲如下更簡單的方式:
CtClass point = ClassPool.getDefault().get("Point");
CtField f = CtField.make("public int z = 0;", point);
point.addField(f);

成員移除

爲了移除字段或者方法,可以調用CtClass類中的removeField或者removeMethod來進行。而移除CtConstructor,可以通過調用removeConstructor方法來進行。

4.4 Annotations

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列表的長度只有一個,且all[0]就是Author對象。名字和年齡這倆註解字段值可以通過調用Author對象中的name方法和year來獲取。
爲了使用getAnnotations方法,類似Author這種註解類型必須被包含在當前的類路徑中,同時必須能夠被ClassPool對象所訪問,如果類的註解類型無法被找到,Javassist就無法獲取此註解類型的默認註解值。

4.5 運行時類支持

在大部分情況下,在Javassist中修改類並不需要Javassist運行時的支持。但是,有些基於Javassist編譯器生成的字節碼,則需要javassist.runtime這種運行時支持類包的支持(更多細節請訪問此包的API)。需要注意的是,javassist.runtime包是Javassist中進行類修改的時候,唯一可能需要調用的包。

4.6導入

所有的源碼中的類名,必須是完整的(必須包含完整的包名),但是java.lang包例外,比如,Javassist編譯器可以將java.lang包下的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。
需要注意的是,importPckage方法不會影響到ClassPool中的get方法操作,只會影響到編譯器的包導入操作。get方法中的參數在任何情況下,必須是完整的,包含包路徑的。

4.7限制

在當前擴展中,Javassist中的Java編譯器有語言層面的幾大限制,具體如下:
不支持J2SE 5.0中的新語法(包括enums和generics)。Javassist底層API纔會支持註解,具體內容可以查看javassist.bytecode.annotation包(CtClass和CtBehavior中的getAnnotations方法)。泛型被部分支持,可以查看後面的章節來了解更詳細的內容。
數組初始化,也就是被雙括號包圍的以逗號分隔的表達式,不支持同時初始化多個。
不支持內部類或者匿名類。需要注意的是,這僅僅是因爲編譯器不支持,所以無法編譯匿名表達式。但是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((B)new C()),但是它依舊會將會調用foo(A)。
推薦用戶使用#號分隔符來分隔類名和靜態方法或者字段名。比如在java中,正常情況下我們會這麼調用:
javassist.CtClass.intType.getName()
我們會訪問javassist.Ctclass中的靜態字段intType,然後調用其getName方法。而在Javassist中,我們可以按照如下的表達式來書寫:
javassist.CtClass#intType.getName()
這樣編譯器就能夠快速的解析此表達式了。
 

5. 字節碼API

爲了直接修改類文件,Javassist也提供了底層的API,想使用這些API的話,你需要有良好的Java字節碼知識儲備和類文件格式的認知,這樣,你使用這些API修改類文件的時候,纔可以隨心所欲而不逾矩。
如果你只是想生成一個簡單的類文件,那麼javassist.bytecode.ClassFileWriter類可以做到。它雖然體積小,但是是比javassist.bytecode.ClassFile更爲快速的存在。
 

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,那麼也能將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.class這個類文件,它包含了對如下類的擴展:
package test;
class Foo implements Cloneable {
    public int width;
}

5.2 添加和刪除成員

ClassFile提供了addField方法和addMethod方法來添加字段或者方法(需要注意的是,在字節碼層面上說來,構造器也被視爲方法),同時也提供了addAttribute方法來爲類文件添加屬性。
需要注意的是FiledInfo,MethodInfo和AttributeInfo對象包含了對ConstPool(const pool table)對象的指向。此ConstPool對象被添加到ClassFile對象中後,在ClassFile對象和FiledInfo對象(或者是MethodInfo對象等)中必須是共享的。換句話說,FiledInfo對象(或者MethodInfo對象等)在不同的ClassFile中是不能共享的。
爲了從ClassFile對象中移除字段或者方法,你必須首先通過類的getFields方法獲取所有的字段以及getMethods方法獲取所有的方法來生成java.util.List對象,然後將此對象返回。之後就可以通過List對象上的remove方法來移除字段或者方法了,屬性的移除方式也不例外,只需要通過FiledInfo或者MethodInfo中的getAttributes方法來獲取到屬性列表後,然後將相關屬性從中移除即可。

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中的一部分:
  • void begin()
    移到第一個指令處.
  • void move(int index)
    移到指定索引處
  • boolean hasNext()
    如果存在指令的話,返回true
  • int next()
    返回下一個指令的索引
    需要注意的是,此方法並不會返回下一個指令的操作碼
  • int byteAt(int index)
    返回指定索引處的無符號8bit位長值.
  • int u16bitAt(int index)
    返回指定索引處的無符號16bit位長值.
  • int write(byte[] code, int index)
    在指定索引處寫入字節數組.
  • void insert(int index, byte[] code)
    在指定索引處寫入字節數組,其他字節碼的offset等將會自適應更改。

下面的代碼段展示了方法體中的所有指令:

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方法來添加8bit操作碼,提供了addIndex方法來添加索引。8bit操作碼的值是在Opcode接口中被定義的。
addOpcode方法和其他添加特殊指令的方法可以自動的維持堆棧的深度,除非操作流程出現了分歧,在這裏,我們可以使用Bytecode的getMaxStack方法來獲取堆棧最大深度。同時,堆棧深度和Bytecode對象內創建的CodeAtrribute對象也有關係,爲了重新計算方法體中的最大堆棧深度,可以使用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 註解 (Meta tags)

註解在運行時態,作爲一個可見或者不可見的屬性被保存在類文件中。它們可以從ClassFile,MethodInfo或者FieldInfo對象中通過getAttribute(AnnotationsAttribute.invisibleTag)方法來獲取。更多的謝潔,可以看看javadoc中關於javassist.bytecode.AnnotationsAttribute類和javassist.bytecode.annotation包的描述。
Javassist也能夠讓你利用一些應用層的API來訪問註解。只需要利用CtClass或者CtBehavior中的的getAnnotations方法接口。
 

6.泛型

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的編譯器不支持泛型,所以利用其編譯的時候,你不得不在調用端做顯式的類型轉換。比如,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();
當使用常規的Java編譯器編譯的時候,類型轉換是不需要的,因爲編譯器會自動進行類型轉換。
如果你想在運行時態,通過反射來訪問類型參數,那麼你不得不在類文件中添加泛型符號。更多詳細信息,請參閱API文檔CtClass中的setGenericSignature方法。
 

7.可變參數

目前,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 });
而不是這樣來:
length(1, 2, 3);

8. J2ME

如果你想在J2ME執行環境中修改類文件,你需要進行預校驗操作,此操作會產生棧Map對象,此對象和JDK1.6中的J2SE棧map表有些相似。當且僅當javassist.bytecode.MethodInfo.doPreverify爲true的時候,Javassist會維護J2ME中的棧map。
你也可以爲修改的方法手動生成一個棧map,比如,一個給定的CtMethod對象中的m,你可以調用如下方法來生成一個棧map:
m.getMethodInfo().rebuildStackMapForME(cpool);
這裏,cpool是ClassPool對象,此對象可以利用CtClass對象中的getClassPool來獲取,它負責利用給定的類路徑來找尋類文件。爲了獲取所有的CtMethods對象,可以通過調用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目錄中。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章