1、讀寫字節(jié)碼
Javassist 是一個能處理 Java字節(jié)碼 的類庫,Java字節(jié)碼存儲在class文件中,每一個class文件都包含了一個Java類或一個接口類。
在Javassist中,使用Javassist.CtClass來表示一個class文件,所以說CtClass類就是用來處理class文件的。舉個例子:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
上面的程序首先獲取了一個ClassPool對象,該對象相當(dāng)于CtClass的一個集合。ClassPool對象在需要時會構(gòu)造一個CtClass對象,并將其記錄下來,當(dāng)下次再次獲取該CtClass對象時就能直接返回。
為了修改類定義,我們需要從ClassPool對象中獲取該類對應(yīng)的CtClass對象。通過ClassPool的get()我們可以獲取到CtClass對象。上面的程序通過ClassPool獲取了一個CtClass對象,該對象代表test. Rectangle這個類。ClassPool的getDefault()方法返回的ClassPool對象,會從默認(rèn)的系統(tǒng)路徑中尋找CtClass對象。
如果我們?nèi)タ?code>ClassPool的源碼,我們會知道,ClassPool就是hashtable,其中key是class對應(yīng)的類名,value是class對應(yīng)的CtClass對象。如果在ClassPool中找不到CtClass對象,則先會new一個CtClass對象,然后將該對象存儲到hashtable中,最后將CtClass對象返回。
CtClass對象是可以被修改的。在上面的程序中,我們將test.Rectangle的父類改成了test.Point。當(dāng) CtClass() 執(zhí)行writeFile()時,這一改動點會寫入到class文件中。writeFile()會將CtClass對象轉(zhuǎn)換成class文件并寫入到本地存儲。Javassist同時還提供了一個直接獲取class對應(yīng)的二進制流的方法,如下所示。
byte[] b = cc.toBytecode();
你還可以將CtClass轉(zhuǎn)換成Class對象。
Class clazz = cc.toClass();
toClass方法會請求當(dāng)前線程的ClassLoader去加載CtClass代表的class類,它返回了一個Class對象表示class類。更多細(xì)節(jié)請查看這一章節(jié)。
定義新類
當(dāng)我們需要定義一個新類時,可以使用ClassPool的makeClass()方法。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
上面的程序定義了一個名為Point的新類,該類不含有任何成員變量。Point類的成員方法可以通過CtNewMethod的工廠方法創(chuàng)建,并通過CtClass的addMethod()方法添加到Point類。
makeClass()方法不能創(chuàng)建一個新的接口類,但是使用makeInterface()方法就可以。接口類中的成員方法可以使用CtNewMethod的abstractMethod()進行創(chuàng)建,因為接口方法就是一個抽象方法。
凍結(jié)類
如果一個CtClass對象通過writeFile()、toClass()、toBytecode()方法轉(zhuǎn)換成class文件,那么Javassist就會將CtClass對象凍結(jié)起來,防止該CtClass對象被修改。也就是說,凍結(jié)了的CtClass對象是不允許被修改的。因為一個類只能被JVM加載一次。
一個已凍結(jié)的CtClass是可以被解凍的,解凍后的CtClass又被允許修改類了,舉個例子:
CtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // OK since the class is not frozen.
上面的程序在defrost()方法被調(diào)用后,CtClass對象又可以被修改了。
如果ClassPool的doPruning成員變量被設(shè)置為true,當(dāng)Javassist凍結(jié)CtClass對象時,會將CtClass對象的內(nèi)部數(shù)據(jù)結(jié)構(gòu)進行裁剪。裁剪掉一些無用的屬性是為了減少內(nèi)存消耗。所以,CtClass對象被裁剪后,方法的字節(jié)碼是不允許被訪問的,但是方法名、方法簽名、注解信息是可以被訪問的。已經(jīng)被裁剪過的CtClass對象不能被解凍了。所以,ClassPool的doPruning成員變量默認(rèn)是false。
stopPruning()方法可以禁止CtClass的裁剪,如下所示。
CtClasss cc = ...;
cc.stopPruning(true);
:
cc.writeFile(); // convert to a class file.
// cc is not pruned.
上面的程序中調(diào)用了stopPruning()方法禁止了CtClass的裁剪操作,因此在writeFile()方法執(zhí)行后,CtClass對象可以被解凍。
注意:在調(diào)試的時候,你可能想在將
class文件寫入到磁盤后,臨時性的禁止CtClass對象的裁剪和凍結(jié)。這個時候你可以調(diào)用debugWriteFile()方法,它會臨時的禁止裁剪,在寫入class文件后會自動解凍CtClass對象,之后該CtClass對象還是可以被裁剪和凍結(jié)的。
類搜索路徑
ClassPool.getDefault()方法默認(rèn)是在JVM的類搜索路徑下返回的ClassPool對象。如果一個程序運行在網(wǎng)頁服務(wù)器上,例如JBoss或Tomcat,那么ClassPool對象可能就找不到用戶所需要的類,因為網(wǎng)頁服務(wù)器使用多個類加載器作為系統(tǒng)類加載器。在這樣的情況下,額外的類路徑必須要注冊到ClassPool中,假設(shè)下面的pool代表一個ClassPool對象:
pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的代碼將this所指向的類對象所在的路徑加入到了ClassPool的class path中。你可以使用任意的類對象替代上面的this.getClass(),這樣類對象的路徑就會注冊到ClassPool的類加載路徑中來。
當(dāng)然,你也可以將一個文件夾注冊到類加載路徑。例如,下面的程序就將/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);
上面的程序?qū)?code>http://www.javassist.org:80/java/加入到了類加載路徑。該URL只能被用來查找屬于org.javassit包目錄下的類。舉個例子,如果要加載org.javassist.test.Main這個類,那么它就可以從下面的路徑中獲?。?/p>
http://www.javassist.org:80/java/org/javassist/test/Main.class
此外,你可以直接將字節(jié)數(shù)組作為ClassPool的類加載路徑,并構(gòu)造出已CtClass兌現(xiàn)。這個時候,你可以使用ByteArrayClassPath??聪旅娴睦樱?/p>
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對象對應(yīng)著byte[] b所表示的類文件。當(dāng)get()方法被調(diào)用時,ClassPool從給定的ByteArrayClassPath讀取類文件,而get()方法中的參數(shù)name必須要ByteArrayClassPath方法的參數(shù)name相匹配。
如果你不知道一個類的全稱路徑,那么你可以使用ClassPool的makeClass方法來得到一個CtClass對象:
ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);
makeClass()方法從給定的輸入流中構(gòu)造出一個CtClass對象。你可以使用makeClass()方法將類文件提供給ClassPool對象。如果搜索路徑中包含大體積的jar包,這有可能會提高性能。由于ClassPool會按需讀取類文件,所以它極有可能會為了查找每一個類文件而重復(fù)查找整個jar包。makeClass()方法可以用來優(yōu)化這樣的搜索。通過makeClass()方法構(gòu)造出來的CtClass對象會被存儲到ClassPool中,而其所在的類文件不會被重復(fù)加載。
用戶可以擴展類的搜索路徑。他們可以定義一個新的類,并實現(xiàn)ClassPath接口,然后調(diào)用ClassPool的insertClassPath()方法。這種方法允許我們將非標(biāo)準(zhǔn)的資源庫加載到類搜索路徑中。
2、ClassPool
ClassPool是CtClass對象的一個容器。一旦CtClass對象被創(chuàng)建出來后,它就永遠(yuǎn)存儲在了ClassPool中。這是因為當(dāng)我們需要編譯一個class的源代碼時,我們需要用到這個class表示的CtClass對象。
舉個例子,假設(shè)我們需要將一個新方法getter()添加到CtClass對象,該CtClass對象代表Point這個類。然后程序需要編譯一段代碼,該代碼中包含Point.getter()方法的調(diào)用,然后將編譯的結(jié)果添加到另外一個名為Line的類的方法中。如果代表Point的CtClass丟失了,那么編譯器就無法編譯getter()這個方法了。注意:Point類中原來并沒有getter()這個方法,因此為了能夠正確編譯該方法,ClassPool必須包含程序運行期間的所有CtClass對象。
避免內(nèi)存溢出
如果CtClass對象非常大,ClassPool就會出現(xiàn)內(nèi)存溢出(雖然這種情況很少發(fā)生,因為Javassist會通過 多種方式 去盡量減少內(nèi)存消耗)。為了避免這樣的情況出現(xiàn),我們可以明確地將一些不必要的CtClass對象從ClassPool中去掉。當(dāng)你調(diào)用CtClass對象的detach方法時,CtClass對象就會從ClassPool中去掉??聪旅娴睦樱?/p>
CtClass cc = ... ;
cc.writeFile();
cc.detach();
在調(diào)用了CtClass的detach方法之后,你不能調(diào)用CtClass的任何方法了。但是,你可以通過調(diào)用ClassPool的get()方法來創(chuàng)建一個新的CtClass對象。當(dāng)你調(diào)用了get()方法后,ClassPool會從類文件中再次讀取內(nèi)容并重新創(chuàng)建一個CtClass對象,該對象會從get()方法中返回。
另外一個避免內(nèi)存溢出的方法是,創(chuàng)建一個新的ClassPool對象來代替舊的ClassPool對象。如果舊的ClassPool對象被回收了,那么它包含的CtClass對象也會被回收。如果我們要創(chuàng)建一個新的ClassPool對象,那么需要執(zhí)行如下代碼:
ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()
上面的程序創(chuàng)建了一個默認(rèn)的ClassPool對象,這和我們使用ClassPool.getDefault()方法創(chuàng)建的ClassPool對象是一樣的。但是,使用ClassPool.getDefault()工廠方法創(chuàng)建ClassPool對象顯得更加方便。
注意:new ClassPool(true)構(gòu)造了一個ClassPool對象并加入了系統(tǒng)的類搜索路徑,它等同于下面的代碼:
ClassPool cp = new ClassPool();
cp.appendSystemPath(); // or append another path by appendClassPath()
級聯(lián)的ClassPools
如果程序正在Web程序服務(wù)器上運行,那么需要創(chuàng)建多個ClassPool實例;對于每一個ClassLoader對象都需要創(chuàng)建一個ClassPool實例。程序應(yīng)該通過ClassPool的構(gòu)造函數(shù)而不是通過getDefault()方法來創(chuàng)建ClassPool對象。
多個ClassPool對象可以像java.lang.ClassLoader一樣級聯(lián),例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
如果調(diào)用child.get()方法,那么子ClassPool首先會將委托給父ClassPool。如果父ClassPool中找不到該class文件,那么子ClassPool會嘗試從./classes文件夾下進行查找。
如果child.childFirstLookup屬性設(shè)置成true,那么子ClassPool會在委托給父ClassPool之前從自己的目錄下查找class文件。例如:
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.appendSystemPath(); // the same class path as the default one.
child.childFirstLookup = true; // changes the behavior of the child.
復(fù)制一個類來定義一個新類
我們可以從一個已經(jīng)存在的類中定義一個新類。程序如下:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
上面的程序首先獲取了Point類對應(yīng)的CtClass對象,然后調(diào)用了CtClass的setName()方法將一個新名字Pair賦值給了CtClass對象。在這個調(diào)用之后,這個CtClass對象所代表的的類的名稱Point被改變成了Pair,類定義的其他部分不變。
注意:CtClass的setName()方法改變了ClassPool對象中的記錄。從ClassPool的實現(xiàn)角度來看,ClassPool對象是CtClass對象的哈希表。setName()方法只是改變了CtClass對象在哈希表的key值,key值從原始類名變成了一個新類名。
因此,如果后續(xù)調(diào)用ClassPool的get("Point")方法,那么該方法肯定不會返回上面的程序的cc對象。這個時候,ClassPool會重新讀取Point.class類,然后構(gòu)建一個新的CtClass對象。這是因為關(guān)聯(lián)Point這個名字的CtClass對象已經(jīng)不存在了??聪旅娴睦樱?/p>
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
CtClass cc1 = pool.get("Point"); // cc1 is identical to cc.
cc.setName("Pair");
CtClass cc2 = pool.get("Pair"); // cc2 is identical to cc.
CtClass cc3 = pool.get("Point"); // cc3 is not identical to cc.
cc1、cc2和cc同一個CtClass對象,然而cc3則不是。注意:在cc.setName("Pair")執(zhí)行完成后,cc和cc1引用的CtClass對象都表示Pair類。
ClassPool對象主要用于維護類和CtClass對象之間的一一對應(yīng)的關(guān)系。Javassit不允許使用兩個不同的CtClass對象來表示同一個類,除非有兩個不同的ClassPool對象。
如果要創(chuàng)建另外一個默認(rèn)的ClassPool對象,那么可以執(zhí)行如下代碼(之前已展示):
ClassPool cp = new ClassPool(true);
如果你有兩個ClassPool對象,這個時候你可以從每一個ClassPool中去獲取代表同一個類的不同CtClass對象。
重命名凍結(jié)類來定義一個新類
一旦CtClass對象通過writeFile()或者toBytecode()轉(zhuǎn)換成一個類文件,那么Javassist會禁止我們對該CtClass對象作進一步的修改。因此,在CtClass代表的Point類被轉(zhuǎn)換成class文件后,你就不能通過執(zhí)行setName()方法來定義一個Point的拷貝類。下面的例子是錯誤的:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair"); // wrong since writeFile() has been called.
為了繞開這個限制,你應(yīng)該調(diào)用ClassPool的getAndRename()方法,如下所示:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");
當(dāng)getAndRename()方法被調(diào)用時,ClassPool首先會讀取Point.class類,然后創(chuàng)建一個新的CtClass對象來表示Point類。然而,它會在哈希表中記錄CtClass之前就將CtClass對象重命名為Pair。因此getAndRename()方法可以在writeFile()或者toBytecode()調(diào)用之后執(zhí)行。
3、Class loader
如果事先就知道哪些類需要修改,最簡便的修改方法如下:
- 通過
ClassPool.get()方法獲取一個CtClass對象。 - 修改
CtClass對象。 - 調(diào)用
CtClass對象的writeFile()或者toBytecode()方法獲得修改后的類文件。
如果一個類是否需要修改是在運行時決定的,那么用戶必須使用類加載器。使用類加載器的javassist可以在運行時修改字節(jié)碼。用戶可以定義他們自己的類加載器,也可以使用Javassist提供的類加載器。
如果在加載時,用戶能夠確定是否要修改某個類,用戶必須使用Javassist與類加載器協(xié)作。Javassist可以使用類加載器在加載時修改字節(jié)碼。用戶可以自定義類加載器,也可以使用Javassist提供的類加載器。
3.1 CtClass類的toClass 方法
CtClass的toClass方法使用當(dāng)前線程的類加載器來加載該CtClass代表的class類。如果要調(diào)用該方法,調(diào)用者必須具有相應(yīng)的權(quán)限,否則會拋出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()方法在Hello類的say()方法中插入了一個println()方法。然后構(gòu)造了一個修改后的Hello類的對象,然后調(diào)用該對象的say()方法。
注意:上面的程序的執(zhí)行有一個前提,那就是在toClass()方法被調(diào)用前,Hello這個類從未被加載過,否則JVM會在toClass()方法被調(diào)用前先加載原始的Hello類,這樣當(dāng)我們?nèi)ゼ虞d修改后的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");
:
}
現(xiàn)在main方法中的第一行就將Hello類加載了,這個時候如果我們再去調(diào)用toClass()方法,就會拋出一個異常,因為類加載器不能同時加載Hello類兩次。
如果上面的程序是運行在JBoss或者Tomcat中,toClass()使用的上下文類加載器就會不適合了。在這種情況下,你有可能會看到ClassCastException異常被拋出。為了避免該異常,你必須給toClass()指定一個合適的類加載器。例如,如果 ‘bean’是你會話的bean對象,那么下面的代碼是可以正常運行的:
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
你可以給toClass()方法傳遞你程序中已使用的類加載器(上面的例子使用了bean對象的類加載器)。
不帶參數(shù)的toClass()方法是比較方便的。如果你需要更多復(fù)雜的功能,你應(yīng)該給toClass()方法傳遞你自定義的類加載器。
3.2 Java的類加載機制
在Java中,多個類加載器是可以共存的,而且每一個類加載器有自己的命名空間。不同的類加載器可以使用同樣的類名加載出兩個不同的類。加載出來的兩個類被認(rèn)為是不同的兩個類。這個特性可以讓我們在一個JVM環(huán)境中運行多個應(yīng)用程序,即使這些應(yīng)用程序包含了相同名稱的不同類。
注意:JVM不允許動態(tài)重復(fù)加載類。一旦一個類加載器加載了一個類,那么它在運行期是不能加載它的修改版本的。因此,你不能在JVM加載了該類后再去改變它的定義。然后,JPDA(Java Platform Debugger Architecture) 卻可以提供有限的能力來重新加載該類。 詳細(xì)信息見3.6節(jié)
如果同一個類文件被兩個不同的類加載器加載,那么JVM會使用相同名字和定義來創(chuàng)建兩個不同類。這兩個類被認(rèn)為是不同的兩個類。既然這兩個類是不同的,那么一個類的對象是不能賦值到另一個類對象的變量的,而這樣的轉(zhuǎn)換會失敗并拋出一個ClassCastException異常。
舉個例子,下面代碼片段會拋出一個異常:
MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj; // this always throws ClassCastException.
Box類被兩個不同的類加載器加載。假設(shè)類加載器CL加載了上面的代碼片段。又因為上面的代碼片段引用了MyClassLoader、Class、Object和Box,這樣CL也加載了這些類(除非它委托給了另外一個類加載器)。因為變量b的類型是Box類型,它被CL加載了。另一方面,myLoader同樣加載了Box類。obj對象就是被myLoader加載出來的。因此,最后一行始終會拋出ClassCastException異常,因為obj變量和b變量代表著不同類型的Box類。
多個類加載器形成了一個樹狀結(jié)構(gòu)。除了bootstrap類加載器外,每一個類加載器都有一個父類加載器。它通常加載子類加載器的類。因為加載一個類會沿著這個層次進行委派,所以即使一個類沒有被請求加載,那么它也有可能會被加載。因此,請求加載類C的類加載器A可能和實際上加載類C的類加載器B不是同一個加載器。為了區(qū)分,我們將前者稱為類C的發(fā)起加載器,將后者稱為類C的實際加載器。
此外,如果CL類加載器被請求來加載類C,但它將加載類C的請求委托給了父類加載器PL,這樣CL加載器就永遠(yuǎn)不會被請求來加載類C中定義的其他類。這樣CL就是不是這些類的發(fā)起加載器。而PL則成為了這些類的發(fā)起加載器,PL被請求來加載它們。
為了理解上面所說的行為,讓我們看下面這個例子:
public class Point { // loaded by PL
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 int getBaseX() { return upperLeft.x; }
:
}
public class Window { // loaded by a class loader L
private Box box;
public int getBaseX() { return box.getBaseX(); }
}
假如Window這個類是被類加載器L加載的,那么Window的發(fā)起加載器和真實的加載器也是L。又因為Window類引用了Box類,那么JVM會請求使用L來加載Box類。我們假設(shè)L將該加載任務(wù)委托給其父類PL,那么Box的發(fā)起類加載器是L,而其真正的加載器是PL。在這種情況下,Point類的發(fā)起類加載器就是PL,因為它的發(fā)起類加載器必須和Box的真正的類加載器相同,因此Point類的請求加載絕對不會委派給L。
接下來,讓我們來看一個稍微修改后的例子:
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();
}
}
現(xiàn)在,Window的定義中也引用了Point類。在這種情況下,如果需要加載Point類,類加載器L必須將該請求委托給加載器PL。你必須避免讓兩個不同類加載器多次加載同一個類。兩個類加載器中的其中一個必須以另一個作為父加載器。
當(dāng)Point類被加載時,如果L沒有將該請求委托給PL,那么withIs()就會拋出ClassCastException異常。因為Box類的真正加載類是PL,而Point類又是被Box類所引用,所以Point類也是被PL類所加載。這是因為,getSize()返回的對象Point是被PL所加載的,然而widthIs()中的變量p是被L所加載的。JVM就會認(rèn)為這兩個是不同的類型,然后拋出一個異常。
這樣的行為的確不是很方便,但是卻是必須的。如果下面這行代碼
Point p = box.getSize();
不拋出異常的話,那么Window類的程序員會破壞Point對象的封裝。舉個例子,被PL加載的Point類的x字段是私有的,然而如果L直接加載具有以下定義的Point類,那么Window類是可以直接訪問Point中的x值的。
public class Point {
public int x, y; // not private
public int getX() { return x; }
:
}
3.3 使用javassist的類加載器
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類設(shè)置為test.Rectangle的父類。然后程序加載了test.Rectangle類,并創(chuàng)建了一個test.Rectangle類的對象。
如果用戶需要在類加載時按需修改類,那么用戶可以添加一個事件監(jiān)聽器到javassist.Loader。添加的事件監(jiān)聽器會在類加載器加載類時被喚醒。事件監(jiān)聽器必須實現(xiàn)如下接口:
public interface Translator {
public void start(ClassPool pool)
throws NotFoundException, CannotCompileException;
public void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException;
}
當(dāng)該事件監(jiān)聽器通過addTranslator()方法被添加到javassist.Loader時,start()方法會被執(zhí)行。當(dāng)javassist.Loader加載類之前,onLoad()方法會被執(zhí)行,我們可以在onLoad()方法中修改將要被加載的類。
舉個例子,下面的事件監(jiān)聽器會在類被加載之前,修改類的修飾符為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()方法中不需要調(diào)用toBytecode()或者writeFile()方法,因為javassist.Loader已經(jīng)在獲取類文件的時候調(diào)用過了這些方法。
要使用MyTranslator對象來運行MyApp應(yīng)用程序,主入口類可以像這樣寫:
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);
}
}
要運行上面的程序,可以執(zhí)行以下命令:
% java Main2 arg1 arg2...
MyApp類、程序中的其他類都會經(jīng)過MyTranslator轉(zhuǎn)換。
注意:MyApp應(yīng)用程序是不能訪問loader類,如Main2、MyTranslator、ClassPool等,因為他們是被其他類加載器加載的。MyApp應(yīng)用程序中的類是被javassist.Loader所加載,但是Main2卻是被Java默認(rèn)的類加載器加載。
javassist.Loader搜索類的順序和java.lang.ClassLoader是不一樣的。Java的ClassLoader首先會將類加載請求委托給父類加載器,當(dāng)父類加載器找不到對應(yīng)的類時,則它再自己加載。但是,javassist.Loader會首先嘗試自己加載該類,它只有在下面這些情況下才會將類加載請求委托給父類加載器:
- 在ClassPool中調(diào)用
get()方法找不到這個類時; - 這些類已經(jīng)通過
delegateLoadingOf()執(zhí)行由父類加載器加載。
此搜索順序允許Javassist加載修改過的類。但是,如果找不到修改的類,該類將會被委托給父類加載器。一旦一個類被父類加載器加載了,那么該類引用的其他類也會被父類加載器加載,這樣他們就永遠(yuǎn)不會被修改了。回想一下,C類引用的所有類都被C類實際加載器加載。如果你的程序無法加載修改的類,你應(yīng)該確保是不是所有使用該類的類都已經(jīng)由javassist加載過了。
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();
}
}
}
假設(shè)MyApp類是一個應(yīng)用程序,如果要執(zhí)行該應(yīng)用程序,第一步需要將該類放置在 ./class 目錄下,它不能被包含在類搜索路徑中,否則MyApp.class將會被系統(tǒng)默認(rèn)的類加載加載(它也是SampleLoader的父類加載器)。./class 目錄通過insertClassPath()方法被加載到了類搜索路徑中。你也可以選擇用別的名字代替./class,然后執(zhí)行下面命令:
% java SampleLoader
類加載器會加載MyApp類(./class/MyApp.class),然后調(diào)用MyApp.main()方法,并將命令行中的參數(shù)傳遞進去。
這是最簡單的使用Javassist的方法,然而,如果你想寫一個更加復(fù)雜點的類加載器,你可能需要Java類加載機制的一些細(xì)節(jié)知識。例如,上面的應(yīng)用程序會將MyApp類放置在一個命名空間中,而SampleLoader類則在另一個命名空間中,因為他們是由不同的類加載器加載的。因此,MyApp類是不能直接訪問SampleLoader類的。
3.5 修改系統(tǒng)類
系統(tǒng)類例如java.lang.String是不能被除系統(tǒng)加載器以外的其他加載器加載的。因此,上面程序中的SampleLoader或者javassist.Loader是不能在運行時修改系統(tǒng)類的。
如果你的應(yīng)用程序想修改系統(tǒng)類,系統(tǒng)類必須被靜態(tài)的修改。例如,下面的程序就將一個新的字段hiddenValue到java.lang.String類中:
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(".");
上面的程序會產(chǎn)生一個新文件 "./java/lang/String.class"。
如果你想要使用修改后的String類運行MyApp程序,則執(zhí)行如下命令:
% java -Xbootclasspath/p:. MyApp arg1 arg2...
假設(shè)MyApp的定義如下:
public class MyApp {
public static void main(String[] args) throws Exception {
System.out.println(String.class.getField("hiddenValue").getName());
}
}
那么我們可以看到修改后的String類被成功加載了,MyApp類打印出了hiddenValue的值。
注意:如果使用上面的技術(shù)來覆蓋rt.jar中的系統(tǒng)類,那么部署該應(yīng)用會違反Java 2 運行時二進制代碼許可協(xié)議。
3.6 在運行時重新加載類
如果JVM在JPDA(Java Platform Debugger Architecture)啟用的時候啟動,那么類可以被動態(tài)的重新加載。在JVM加載了一個類后,舊版本的類可以被卸載,新版本的類就可以被重新加載了。這樣,類定義就可以在運行時被動態(tài)的修改。然而,新的類定義必須和舊的類定義有所兼容。JVM是不允許兩個版本的類的模式修改的。他們必須有相同的方法和字段。
Javassist提供了一個類,這使得在運行時重新加載類變得更加方便。如果想要獲取更多關(guān)于運行時重新加載類的信息,可以看API文檔中關(guān)于javassist.tools.HotSwapper的說明。
4、參考文檔
1、英文原文