Javassist之字節(jié)碼讀寫

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ì)再被讀取。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,711評(píng)論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,355評(píng)論 25 708
  • 不管在上學(xué)還是在工作,相信每個(gè)人都會(huì)遇到這樣的狀況~身邊的親戚朋友包括父母,都會(huì)打著“為你好”的名義對(duì)你所做...
    一夢千年花已落閱讀 430評(píng)論 0 1
  • 每個(gè)人的心里,都有自己的一個(gè)大魚海棠。 我叫海棠,從小在大漁村里長大。 大漁村是大澤邊上的一個(gè)小村莊,這里幾乎與世...
    廿八粥閱讀 534評(píng)論 0 0
  • 關(guān)于wkhtmltox,是一個(gè)可以把HTML轉(zhuǎn)換為圖片和pdf的工具。 不多介紹了,詳見官網(wǎng) https://wk...
    Joepis閱讀 11,722評(píng)論 0 14

友情鏈接更多精彩內(nèi)容