BCEL 介紹:
Byte Code Engineering Library (BCEL),這是Apache Software Foundation 的Jakarta 項目的一部分。
BCEL
是 Java classworking 最廣泛使用的一種框架,它可以讓您深入 JVM 匯編語言進行類操作的細節。BCEL與Javassist
有不同的處理字節碼方法,BCEL在實際的JVM 指令層次上進行操作(BCEL擁有豐富的JVM 指令級支持)而Javassist
所強調的源代碼級別的工作。
正在裝載數據……
http://sourceforge.net/projects/bcel
Javassist:使字節碼工程變得簡單(Zz) :
字節碼驅動通常被用來執行對于已經編譯好的類的修改,或者由程序自動創建執行類等等等等相關方面的操作。這就要求字節碼引擎具備無論是在運行時或是編譯時都能修改程序的能力。當下有些技術便是使用字節碼來強化已經存在的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是CtClass 的創建工廠。它在class path中查找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以獲得相關的信息
使用Javassist對.class文件進行修改(Zz) :
最 近重新再看<Inside JVM>,對JAVA編譯成的字節碼結構很感興趣,希望找個工具能夠對.class文件進行的解析和查看。沒找到,倒發現javaassist可以 對字節碼進行操作和修改。此工具是JBOSS項目的一部分,JBOSS實現AOP的基礎。呵呵,開眼界了,原來我們可以直接對字節碼文件進行修改,哪怕不 知道源文件(跟反編譯完全不同)。一個簡單例子:
import javassist.*;
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.setBody("{System.out.println(""shit"");}");
m.insertBefore("System.out.println(""fuck"");");
Class c = cc.toClass();
Hello h = (Hello)c.newInstance();
h.say();
}
}
編譯運行此文件,輸出:
fuck
shit
我們在
CtMethod m = cc.getDeclaredMethod("say");
m.setBody("{System.out.println(""shit"");}");
m.insertBefore("System.out.println(""fuck"");");
修改了say()方法,改成了
System.out.println("fuck");
System.out.println("shit");
這里的ClassPool是CtClass的容器,它讀取class文件,并根據要求保存CtClass的結構以便日后使用,默認狀態下是從當前的類裝載器獲得,當然你可以指定:
pool.insertClassPath("/usr/local/javalib");
當然,不僅僅是修改方法,你還可以新建一個class,利用makeClass()方法,如:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
還可以新增方法,下面是sample里的一個例子,同樣的:
package sample;
import javassist.*;
import java.lang.reflect.*;
/*
A very simple sample program
This program overwrites sample/Test.class (the class file of this
class itself) for adding a method g(). If the method g() is not
defined in class Test, then this program adds a copy of
f() to the class Test with name g(). Otherwise, this program does
not modify sample/Test.class at all.
To see the modified class definition, execute:
% javap sample.Test
after running this program.
*/
public class Test {
public int f(int i) {
i++;
return i;
}
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("sample.Test");
Test test=new Test();
Class c=test.getClass();
Method []method=c.getDeclaredMethods();
for(int i=0;i<method.length;i++){
System.out.println(method[i]);
}
try {
cc.getDeclaredMethod("g");
System.out.println("g() is already defined in sample.Test.");
}
catch (NotFoundException e) {
/* getDeclaredMethod() throws an exception if g()
* is not defined in sample.Test.
*/
CtMethod fMethod = cc.getDeclaredMethod("f");
CtMethod gMethod = CtNewMethod.copy(fMethod, "g", cc, null);
cc.addMethod(gMethod);
cc.writeFile(); // update the class file
System.out.println("g() was added.");
}
}
}
第一次運行時,因為Test里并沒有g()方法,所以執行
CtMethod fMethod = cc.getDeclaredMethod("f");
CtMethod gMethod = CtNewMethod.copy(fMethod, "g", cc, null); //把f方法復制給g
cc.addMethod(gMethod);
cc.writeFile(); //更新class文件
System.out.println("g() was added.");
打印:g() was added
第2次運行時,因為以上步驟已經在class文件中增加了一個g方法,所以
System.out.println("g() is already defined in sample.Test.");
打印:g() is already defined in sample.Test
Javassist不僅能修改你自己的class文件,而且可以同樣修改JDK自帶的類庫(廢話,類庫也是人寫的^_^)具體請看它的tutorial。
Javassist 官方網站: http://www.csg.is.titech.ac.jp/~chiba/javassist/