1. 程式人生 > >javassist 編譯器位元組碼增強

javassist 編譯器位元組碼增強

Javassist是一個執行位元組碼操作的強而有力的驅動程式碼庫。它允許開發者自由的在一個已經編譯好的類中新增新的方法,或者是修改已有的方法。但是, 和其他的類似庫不同的是,Javassist並不要求開發者對位元組碼方面具有多麼深入的瞭解,同樣的,它也允許開發者忽略被修改的類本身的細節和結構。
字 節碼驅動通常被用來執行對於已經編譯好的類的修改,或者由程式自動建立執行類等等等等相關方面的操作。這就要求位元組碼引擎具備無論是在執行時或是編譯時都 能修改程式的能力。當下有些技術便是使用位元組碼來強化已經存在的Java類的,也有的則是使用它來使用或者產生一些由系統在執行時動態建立的類。舉例而 言,JDO1.0規範就使用了位元組碼技術對資料庫中的表進行處理和預編譯,並進而包裝成Java類。特別是在面向物件驅動的系統開發中,相當多的框架體系 使用位元組碼以使我們更好的獲得程式的範型性和動態性。而某些EJB容器,比如JBOSS專案,則通過在執行中動態的建立和載入EJB,從而戲劇性的縮短了 部署EJB的週期。這項技術是如此的引人入勝,以至於在JDK中也有了標準的java.lang.reflect.Proxy類來執行相關的操作。

但是,儘管如此,編寫位元組碼對於框架程式開發者們而言,卻是一個相當不受歡迎的繁重任務。學習和使用位元組碼在某種程度上就如同使用匯編語言。這使得於大多數 開發者而言,儘管在程式上可以獲得相當多的好處,可攀登它所需要的難度則足以冷卻這份熱情。不僅如此,在程式中使用位元組碼操作也大大的降低了程式的可讀性 和可維護性。

這是一塊很好的奶油麵包,但是我們卻只能隔著櫥窗流口水 難道我們只能如此了嗎?

所幸的是,我們還有Javassist。Javassist是一個可以執行位元組碼操作的函式庫,可是儘管如此,它卻是簡單而便與理解的。他允許開發者對自己的程式自由的執行位元組碼層的操作,當然了,你並不需要對位元組碼有多深的瞭解,或者,你根本就不需要了解。

API Parallel to the Reflection API

Javassist 的最外層的API和JAVA的反射包中的API頗為類似。它使你可以在裝入ClassLoder之前,方便的檢視類的結構。它主要由 CtClass,,CtMethod,,以及CtField幾個類組成。用以執行和JDK反射API中 java.lang.Class,,java.lang.reflect.Method,, java.lang.reflect.Method .Field相同的操作。這些類可以使你在目標類被載入前,輕鬆的獲得它的結構,函式,以及屬性。此外,不僅僅是在功能上,甚至在結構上,這些類的執行函 數也和反射的API大體相同。比如getName,getSuperclass,getMethods,,getSignature,等等。如果你對 JAVA的反射機制有所瞭解的話,使用Javassist的這一層將會是輕鬆而快樂的。


接下來我們將給出一個使用Javassist來讀取org.geometry.Point.class的相關資訊的例子(當然了,千萬不要忘記引入javassist.*包):


1. ClassPool pool = ClassPool.getDefault();


2. CtClass pt = pool.get("org.geometry.Point");


3. System.out.println(pt.getSuperclass().getName());


其中,ClassPool是CtClas 的建立工廠。它在classpath中查詢CtClass的位置,併為每一個分析請求建立一個CtClass例項。而“getSuperclass().getName()”則展示 出org.geometry.Point.class所繼承的父類的名字。

但是,和反射的API不盡相同的 是,Javassist並不提供構造的能力,換句話說,我們並不能就此得到一個org.geometry.Point.class類的例項。另一方面,在該類沒有例項化前,Javassist也不提供對目標類的函式的呼叫介面和獲取屬性的值的方法。在分析階段,它僅僅提供對目標類的類定義修改,而這點,卻是反射API所無法做到的。


舉例如下:


4. pt.setSuperclass(pool.get("Figure"));


這樣做將修改目標類和其父類之間的關係。我們將使org.geometry.Point.clas改繼承自Figure類。當然了,就一致性而言,必須確保Figure類和原始的父類之間的相容性。


而往目標類中新增一個新的方法則更加的簡單了。首先我們來看位元組碼是如何形成的:


5. CtMethod m = CtNewMethod.make("public int xmove(int dx) { x += dx; }", pt);


6. pt.addMethod(m);


CtMethod類的讓我們要新增一個方法只需要寫一段小小的函式。這可是一個天大的好訊息,開發者們再也不用為了實現這麼一個小小的操作而寫一大段的虛擬機器指令序列了。Javassist將使用一個它自帶的編譯器來幫我們完成這一切。


最後,千萬別忘了指示Javassist把已經寫好的位元組碼存入到你的目標類裡:


7. pt.writeFile();


writeFile方法可以幫我們把修改好了的定義寫到目標類的.class檔案裡。當然了,我們甚至可以在該目標類載入的時候完成這一切,Javassist可以很好的和ClassLoader協同工作,我們不久就將看到這一點。


Javassist 並不是第一套用以完成從程式碼到位元組碼的翻譯的函式庫。Jakarta的BCEL也是一個比較知名的位元組碼引擎工具。但是,你卻無法使用BCEL來完成程式碼 級別的字元碼操作。如果你需要在一個已經編譯好的類中新增一個新的方法,假如你用的是BCEL的話,你只能定義一段由那麼一大串字元碼所構成的指令序列。 正如上文所說,這並不是我們所希望看到的。因此,就此方面而言,Javassis使用程式碼的形式來插入新的方法實在是一大福音。


Instrumenting a Method Body


和 方法的新增一樣,對於一個類的方法的其他操作也是定義在程式碼層上的。換而言之,儘管這些步驟是必須的,開發者們也同樣無須直接對虛擬機器的指令序列進行操作 和修改,Javassis將自動的完成這些操作。當然了,如果開發者認為自己有必要對這些步驟進行管理和監控,或者希望由自己來管理這些操作的 話,Javassist同樣提供了更加底層的API來實現,不過我們在這篇文章中將不會就此話題再做深入探討。恩,儘管從結構而言,它和BCEL的位元組碼 層API差不多。


設計Javassist對目標類的子函式體的操作API的設想立足與ASPect-Oriented Programming(AOP)思想。Javassist允許把具有耦合關係的語句作為一個整體,它允許在一個插入語句中呼叫或獲取其他函式或者及屬性 值。它將自動的對這些語句進行優先順序分解並執行巢狀操作。


如下例所示,清單1首先包含了一個CtMethod,它主要針對 Screen類的draw方法。然後,我們定義一個Point類,該類有一個move操作,用來實現該Point的移動。當然了,在移動前,我們希望可以 通過draw方法得到該point目前的位置,那麼,我們需要對該move方法加增如下的定義:


{ System.out.println("move"); $_ = $proceed($$); }


這樣,在執行move之前,我們就可以打印出它的位置了。請注意這裡的呼叫語句,它是如下格式的:


$_ = $proceed($$);


這樣我們就將使用原CtMethod類中的process()對該point的位置進行追蹤了。


基 與如上情況,CtMethod的關於methord的操作其實被劃分成瞭如下步驟,首先,CtMethod的methord將掃描插入語句(程式碼)本身。 一旦發現了子函式,則建立一個ExprEditor例項來分析並執行這個子函式的操作。這個操作將在整個插入語句執行之前完成。而假如這個例項存在某個 static的屬性,那麼methord將率先檢測對插入語句進行檢測。然後,在執行插入到目標類---如上例的point類---之前,該static 屬性將自動的替換插入語句(程式碼)中所有的相關的部分。不過,值得注意的是,以上的替換操作,將在Javassist把插入語句(程式碼)轉變為位元組碼之後 完成。


Special Variables


在替換的語句(程式碼)中,我們也有可能需要用到一些特殊變數 來完成對某個子函式的呼叫,而這個時候我們就需要使用關鍵字“$”了。在Javassist中,“$”用來申明此後的某個詞為特殊引數,而“$_”則用來 申明此後的某個詞為函式的回傳值。每一個特殊引數在被呼叫時應該是這個樣子的“$1,$2,$3 ”但是,特別的,目標類本身在被呼叫時,則被表示為 “$0”。這種使用格式讓開發者在填寫使用子函式的引數時輕鬆了許多。比如如下的例子:


{ System.out.println("move"); $_ = $proceed($1, 0); }


請注意,該子函式的第2個引數為0。


另 外一個特殊型別則是$arg,它實際上是一個容納了函式所有呼叫引數的Object佇列。當Javassist在掃描該$arg時,如果發現某一個引數為 JAVA的基本型別,則它將自動的對該引數進行包裝,並放入佇列。比如,當它發現某一個引數為int型別時,它將使用java.lang.integer 類來包裝這個int引數,並存入引數佇列。和Java的反射包:java.lang.reflect.Methord類中的invoke方法相 比,$args明顯要省事的多。


Javassist也同樣允許開發者在某個函式的頭,或者某個函式的尾上插入某段語句(程式碼)。比如,它有一個insertBefore方法用以在某函式的呼叫前執行某個操作,它的使用大致是這個樣子的:


1. ClassPool pool = ClassPool.getDefault();
2. CtClass cc = pool.get("Screen");
3. CtMethod cm = cc.getDeclaredMethod("draw", new CtClass[0]);
4. cm.insertBefore("{ System.out.println($1); System.out.println($2); }");
5. cc.writeFile();


以上例子允許我們在draw函式呼叫之前執行列印操作---把傳遞給draw的兩個引數打印出來。


同樣的,我們也可以使用關鍵字$對某一個函式進行修改或者是包裝,下面就


1. CtClass cc = sloader.get("Point");
2. CtMethod m1 = cc.getDeclaredMethod("move");
3. CtMethod m2 = CtNewMethod.copy(m1, cc, null);
4. m1.setName(m1.getName() + "_orig");
5. m2.setBody("{ System.out.println("call"); return $proceed($$);
}", "this", m1.getName());
6. cc.addMethod(m2);
7. cc.writeFile();


以上程式碼的前四行不難理解,Javassist首先對Point中的move方法做了個拷貝,並建立了一個新的函式。然後,它把存在與Point類中的原 move方法更名為“_orig”。接下來,讓我們關注一下程式第五行中的幾個引數:第一個引數指示該函式的在執行的最初部分需要先列印一段資訊,然後執 行子函式proceed()並返回結果,這個和move方法差不多,很好理解。第二個引數則只是申明該子函式所在的類的位置。這裡為this即為 Point類本身。第三個引數,也就是“m1.getName()”則定義了這個新函式的名字。


Javassist也同樣具有其他的操作和類來幫助你實現諸如修改某一個屬性的值,改變函式的回值,並在某個函式的執行後補上其他操作的功能。您可以瀏覽www.javassist.org以獲得相關的資訊。