Javassist是一個(gè)用于處理Java字節(jié)碼的類庫。Java字節(jié)碼是一個(gè)以二進(jìn)制文件進(jìn)行存儲(chǔ)的class文件。每一個(gè)class文件都包含一個(gè)Javal類或者是接口。
Javassist.Ctclass是一個(gè)對(duì)class文件的一個(gè)抽象表示形式。一個(gè)CtClass(編譯時(shí)類)對(duì)象是一個(gè)處理class文件的句柄。下面這段程序是一個(gè)非常簡單的例子:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
這段程序首先獲取一個(gè)ClassPool對(duì)象,它控制著Javassist對(duì)字節(jié)碼的修改。ClassPool對(duì)象是一個(gè)表示class文件的CtClass對(duì)象的容器。它讀取一個(gè)類文件,用于構(gòu)造CtClass對(duì)象,并記錄構(gòu)建的對(duì)象以響應(yīng)后面的訪問。
要去改變一個(gè)類的定義,開發(fā)者必須要先從ClassPool對(duì)象中,獲得一個(gè)表示該類的CtClass對(duì)象的引用。該類可以通過ClassPool的get()方法獲取。在上面的展示的程序示例中,調(diào)用ClassPool的get()方法,返回了一個(gè)用于表示test.Rectangle類的CtClass對(duì)象cc。ClassPool.getDefault()方法的作用是掃描默認(rèn)系統(tǒng)路徑的類,返回一個(gè)ClassPool對(duì)象。
站在源碼的角度,ClassPool是一個(gè)使用類的名稱當(dāng)作key,對(duì)應(yīng)的CtClass為value的hash表。ClassPool的get()方法根據(jù)傳入的類名參數(shù),在hash表中查詢到該類名對(duì)應(yīng)的CtClass對(duì)象并返回。如果沒有查詢到該類名對(duì)應(yīng)的CtClass對(duì)象,get()方法將會(huì)讀取類文件來構(gòu)造一個(gè)新的CtClass對(duì)象,這個(gè)對(duì)象會(huì)被添加到hash表中,然后返回。
從ClassPool對(duì)象中獲取的CtClass對(duì)象是可以被改變的(關(guān)于如何修改CtClass的詳細(xì)說明將會(huì)在下文展示)。在上面的例子中,test.Rectangle的父類被改變成了test.Point。這個(gè)改變?cè)谧詈笳{(diào)用CtClass()的writeFile()方法時(shí)會(huì)被寫入源class文件。
writeFile()方法將CtClass對(duì)象轉(zhuǎn)變成一個(gè)class文件,并寫入本地磁盤。Javassist也提供了直接獲取修改過的字節(jié)碼的方法。調(diào)用toBytecode()方法,直接獲取字節(jié)碼:
byte[] b = cc.toBytecode();
也可以直接讀取CtClass:
Class clazz = cc.toClass();
toClass()方法會(huì)請(qǐng)求當(dāng)前線程的上下文類加載器去加載CtClass表示的類文件。它返回一個(gè)表示被加載類的java.lang.Class對(duì)象。更加詳細(xì)的說明,請(qǐng)看下面的部分。
定義一個(gè)新的類
要重新定義一個(gè)類,必須調(diào)用ClassPool的makeClass()方法。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
這段代碼定義了一個(gè)Point類,它沒有包含任何的屬性或方法。Point類可以通過調(diào)用CtNewMethod的工廠方法來創(chuàng)建方法,并使用CtClass的addMethod()將該方法添加到Point類中。
makeClass()無法創(chuàng)建一個(gè)新的接口,但ClassPool的makeInterface()方法可以。接口的方法可以通過CtNewMethod的abstractMethod()方法創(chuàng)建。注意,一個(gè)接口方法是一個(gè)抽象的方法。
類的凍結(jié)
如果一個(gè)CtClass對(duì)象通過writeFile(),toClass(),或者toBytecode()轉(zhuǎn)換到一個(gè)類文件,Javassist將會(huì)凍結(jié)CtClass對(duì)象。對(duì)CtClass對(duì)象的進(jìn)一步改變將不被允許。因?yàn)镴VM不會(huì)允許重新加載一個(gè)類,所以當(dāng)開發(fā)者嘗試去改變一個(gè)已經(jīng)加載的類時(shí),將會(huì)發(fā)出警告。
一個(gè)凍結(jié)的CtClass可以被解除,解凍之后就可以允許對(duì)類的定義進(jìn)行修改。例如下面這個(gè)例子:
CtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // OK since the class is not frozen.
在調(diào)用defrost()方法之后,CtClass對(duì)象就可以再次被修改。
如果ClassPool.doPruning被設(shè)置成true,在Javassist凍結(jié)CtClass對(duì)象的時(shí)候,Javassist將會(huì)對(duì)CtClass對(duì)象包含的數(shù)據(jù)結(jié)構(gòu)進(jìn)行精簡。通過修剪丟棄對(duì)象不需要的屬性(attribute_info結(jié)構(gòu))來減少內(nèi)存的消耗。例如,丟棄Code_attribute結(jié)構(gòu)(方法體)。因此,當(dāng)CtClass對(duì)象被精簡之后,方法的字節(jié)碼除了方法名,簽名,以及注解之外將不能被訪問。精簡的CtClass對(duì)象無法再次被解凍。ClassPool.doPruning的默認(rèn)值是false。
對(duì)于一些并不想被精簡的特殊CtClass,必須提前調(diào)用stopPruning():
CtClasss cc = ...;
cc.stopPruning(true);
:
cc.writeFile(); // convert to a class file.
// cc is not pruned.
CtClass對(duì)象cc沒有被精簡。因此它可以在writeFile()被調(diào)用之后解凍。
注意:在進(jìn)行debug的時(shí)候,可能需要暫時(shí)停止精簡或者凍結(jié)或者將修改過的類文件寫入磁盤。debugWriteFile()方法可以方便的實(shí)現(xiàn)此功能。它可以停止精簡,寫類文件,解凍,同時(shí)再次精簡。
類掃描路徑
由靜態(tài)方法ClassPool.getDefault()返回的默認(rèn)ClassPool與JVM有相同的掃描路徑。如果程序在JBoss或者Tomcat之類的web應(yīng)用程序中,ClassPoll對(duì)象可能無法找到用戶的類,因?yàn)檫@些Web應(yīng)用服務(wù)器使用多個(gè)類加載器以及系統(tǒng)類加載器。在這種情況下,就必須注冊(cè)額外的類掃描路徑到ClassPool中??梢允褂萌缦路绞竭M(jìn)行注冊(cè):
pool.insertClassPath(new ClassClassPath(this.getClass()));
這行代碼注冊(cè)了一類路徑,它被用來加載this所指代的對(duì)象的類。this.getClass()參數(shù)可以被替換成任何一個(gè)類對(duì)象。用于加載由已經(jīng)注冊(cè)的該類對(duì)象表示的類的類路徑。
你可以注冊(cè)一個(gè)目錄名稱來作為類掃描路徑。例如,下面的代碼將/usr/local/javalib添加到類掃描路徑:
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath("/usr/local/javalib");
用戶可以添加的掃描路徑不僅僅是目錄,也可以是一個(gè)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/"添加到類掃描路徑。這個(gè)URL僅僅被用來掃描屬于org.javassist包的類。例如,想要加載一個(gè)org.javassist.test.Main,它的類文件可以這樣獲得:
http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你可以直接傳入一個(gè)字節(jié)數(shù)組到ClassPool對(duì)象中,由這個(gè)數(shù)組構(gòu)造出一個(gè)CtClass對(duì)象??梢允褂肂yteArrayClassPath來做到。例如:
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對(duì)象表示由b所引用的類文件定義的類。如果調(diào)用了get()方法,ClassPool會(huì)從給定的ByteArrayClassPath中讀取一個(gè)類文件,傳入給get()的類名參數(shù)與所指定的類名相同。
如果你不知道完整的類名,那么你可以使用ClassPool的makeClass():
ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);
makeClass()方法返回一個(gè)由所給的輸入流構(gòu)造的CtClass對(duì)象。你可以使用makeClass()將類文件直接傳入到ClassPool對(duì)象中。如果掃描路徑包含很大的jar文件,這可能會(huì)提升性能。因?yàn)镃lassPool對(duì)象只會(huì)在需要的時(shí)候才會(huì)讀取類文件,所以它可能會(huì)因?yàn)槊恳粋€(gè)類文件而重復(fù)掃描整個(gè)jar文件。makeClass()可以被用來優(yōu)化重復(fù)掃描的情況。由makeClass()構(gòu)造的CtClass將一直被ClassPool對(duì)象所持有,所對(duì)應(yīng)的類文件也將不會(huì)再被讀取。