本文譯自: Javassist Tutorial-1
原作者: Shigeru Chiba
完成時間:2016年11月
1. 讀寫字節(jié)碼
我們知道 Java 字節(jié)碼以二進制的形式存儲在 class 文件中,每一個 class 文件包含一個 Java 類或接口。Javaassist 就是一個用來處理 Java 字節(jié)碼的類庫绒疗。
在 Javassist 中,類 Javaassit.CtClass
表示 class 文件皆辽。一個 GtClass (編譯時類)對象可以處理一個 class 文件因苹,下面是一個簡單的例子:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("test.Rectangle");
cc.setSuperclass(pool.get("test.Point"));
cc.writeFile();
這段代碼首先獲取一個 ClassPool 對象移国。ClassPool 是 CtClass 對象的容器。它按需讀取類文件來構造 CtClass 對象荷逞,并且保存 CtClass 對象以便以后使用媒咳。
為了修改類的定義,首先需要使用 ClassPool.get() 方法來從 ClassPool 中獲得一個 CtClass 對象种远。上面的代碼中辣卒,我們從 ClassPool 中獲得了代表 test.Rectangle 類的 CtClass 對象的引用瞭亮,并將其賦值給變量 cc枷恕。使用 getDefault() 方法獲取的 ClassPool 對象使用的是默認系統(tǒng)的類搜索路徑嫂易。
從實現(xiàn)的角度來看斧抱,ClassPool 是一個存儲 CtClass 的 Hash 表常拓,類的名稱作為 Hash 表的 key。ClassPool 的 get() 函數(shù)用于從 Hash 表中查找 key 對應的 CtClass 對象辉浦。如果沒有找到弄抬,get() 函數(shù)會創(chuàng)建并返回一個新的 CtClass 對象,這個新對象會保存在 Hash 表中宪郊。
從 ClassPool 中獲取的 CtClass 是可以被修改的(稍后會討論細節(jié))掂恕。
在上面的例子中,test.Rectangle 的父類被設置為 test.Point弛槐。調用 writeFile() 后懊亡,這項修改會被寫入原始類文件。writeFile() 會將 CtClass 對象轉換成類文件并寫到本地磁盤乎串。也可以使用 toBytecode() 函數(shù)來獲取修改過的字節(jié)碼:
byte[] b = cc.toBytecode();
你也可以通過 toClass() 函數(shù)直接將 CtClass 轉換成 Class 對象:
Class clazz = cc.toClass();
toClass() 請求當前線程的 ClassLoader 加載 CtClass 所代表的類文件店枣。它返回此類文件的 java.lang.Class 對象,更多細節(jié)叹誉,請參考下面的章節(jié)鸯两。
定義新類
使用 ClassPool 的 makeClass() 方法可以定義一個新類。
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Point");
這段代碼定義了一個空的 Point 類长豁。Point 類的成員方法可以通過 CtNewMethod 類的工廠方法來創(chuàng)建钧唐,然后使用 CtClass 的 addMethod() 方法將其添加到 Point 中。
使用 ClassPool 中的 makeInterface() 方法可以創(chuàng)建新接口匠襟。接口中的方法可以使用 CtNewMethod 的 abstractMethod() 方法創(chuàng)建钝侠。
將類凍結
如果一個 CtClass 對象通過 writeFile(), toClass(), toBytecode() 被轉換成一個類文件,此 CtClass 對象會被凍結起來酸舍,不允許再修改机错。因為一個類只能被 JVM 加載一次。
但是父腕,一個冷凍的 CtClass 也可以被解凍弱匪,例如:
CtClasss cc = ...;
:
cc.writeFile();
cc.defrost();
cc.setSuperclass(...); // 因為類已經被解凍,所以這里可以調用成功
調用 defrost() 之后,此 CtClass 對象又可以被修改了萧诫。
如果 ClassPool.doPruning 被設置為 true斥难,Javassist 在凍結 CtClass 時,會修剪 CtClass 的數(shù)據(jù)結構帘饶。為了減少內存的消耗哑诊,修剪操作會丟棄 CtClass 對象中不必要的屬性。例如及刻,Code_attribute 結構會被丟棄镀裤。一個 CtClass 對象被修改之后,方法的字節(jié)碼是不可訪問的缴饭,但是方法名稱暑劝、方法簽名、注解信息可以被訪問颗搂。修剪過的 CtClass 對象不能再次被解凍担猛。ClassPool.doPruning 的默認值為 false。
stopPruning() 可以用來駁回修剪操作丢氢。
CtClasss cc = ...;
cc.stopPruning(true);
:
cc.writeFile(); // 轉換成一個 class 文件
// cc is not pruned.
這個 CtClass 沒有被修剪傅联,所以在 writeFile() 之后,可以被解凍疚察。
注意:調試的時候蒸走,你可能臨時需要停止修剪和凍結,然后保存一個修改過的類文件到磁盤貌嫡,debugWriteFile() 方法正是為此準備的载碌。它停止修剪,然后寫類文件衅枫,然后解凍并再次打開修剪(如果開始時修養(yǎng)是打開的)嫁艇。
類搜索路徑
通過 ClassPool.getDefault() 獲取的 ClassPool 使用 JVM 的類搜索路徑。如果程序運行在 JBoss 或者 Tomcat 等 Web 服務器上弦撩,ClassPool 可能無法找到用戶的類步咪,因為 Web 服務器使用多個類加載器作為系統(tǒng)類加載器。在這種情況下益楼,ClassPool 必須添加額外的類搜索路徑猾漫。
下面的例子中,pool 代表一個 ClassPool 對象:
pool.insertClassPath(new ClassClassPath(this.getClass()));
上面的語句將 this 指向的類添加到 pool 的類加載路徑中感凤。你可以使用任意 Class 對象來代替 this.getClass()悯周,從而將 Class 對象添加到類加載路徑中。
也可以注冊一個目錄作為類搜索路徑陪竿。下面的例子將 /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);
上述代碼將 http://www.javassist.org:80/java/ 添加到類搜索路徑。并且這個URL只能搜索 org.javassist
包里面的類。例如闰挡,為了加載 org.javassist.test.Main
锐墙,它的類文件會從獲取 http://www.javassist.org:80/java/org/javassist/test/Main.class 獲取。
此外长酗,也可以直接傳遞一個 byte 數(shù)組給 ClassPool 來構造一個 CtClass 對象溪北,完成這項操作,需要使用 ByteArrayPath 類夺脾。示例:
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 對象表示 b 代表的 class 文件之拨。將對應的類名傳遞給 ClassPool 的 get() 方法,就可以從 ByteArrayClassPath 中讀取到對應的類文件咧叭。
如果你不知道類的全名蚀乔,可以使用 makeClass() 方法:
ClassPool cp = ClassPool.getDefault();
InputStream ins = an input stream for reading a class file;
CtClass cc = cp.makeClass(ins);
makeClass() 返回從給定輸入流構造的 CtClass 對象。 你可以使用 makeClass() 將類文件提供給 ClassPool 對象佳簸。如果搜索路徑包含大的 jar 文件乙墙,這可能會提高性能颖变。由于 ClassPool 對象按需讀取類文件生均,它可能會重復搜索整個 jar 文件中的每個類文件。 makeClass() 可以用于優(yōu)化此搜索腥刹。由 makeClass() 構造的 CtClass 保存在 ClassPool 對象中马胧,從而使得類文件不會再被讀取。
用戶可以通過實現(xiàn) ClassPath 接口來擴展類加載路徑衔峰,然后調用 ClassPool 的 insertClassPath() 方法將路徑添加進來佩脊。這種技術主要用于將非標準資源添加到類搜索路徑中。
2. ClassPool
ClassPool 是 CtClass 對象的容器垫卤。因為編譯器在編譯引用 CtClass 代表的 Java 類的源代碼時威彰,可能會引用 CtClass 對象,所以一旦一個 CtClass 被創(chuàng)建穴肘,它就被保存在 ClassPool 中.
例如歇盼,一個 CtClass 類代表 Point 類,并給 CtClass 添加 getter() 方法评抚。然后豹缀,程序嘗試編譯一段代碼,代碼中包含了 Point 的 getter() 調用慨代,然后將這段代碼添加了另一個類 Line 中邢笙,如果代表 Point 的 CtClass 丟失,編譯器就無法編譯 Line 中的 Point.getter() 方法侍匙。注:原來的 Point 類中無 getter() 方法氮惯。因此,為了能夠正確編譯這個方法調用,ClassPool 必須在程序執(zhí)行期間包含所有的 CtClass 實例筐骇。
避免內存溢出
如果 CtClass 對象的數(shù)量變得非常大(這種情況很少發(fā)生债鸡,因為 Javassist 試圖以各種方式減少內存消耗),ClassPool 可能會導致巨大的內存消耗铛纬。 為了避免此問題厌均,可以從 ClassPool 中顯式刪除不必要的 CtClass 對象。 如果對 CtClass 對象調用 detach()告唆,那么該 CtClass 對象將被從 ClassPool 中刪除棺弊。 例如:
CtClass cc = ... ;
cc.writeFile();
cc.detach();
在調用 detach() 之后,就不能調用這個 CtClass 對象的任何方法了擒悬。但是如果你調用 ClassPool 的 get() 方法模她,ClassPool 會再次讀取這個類文件,創(chuàng)建一個新的 CtClass 對象懂牧。
另一個辦法是用新的 ClassPool 替換舊的 ClassPool侈净,并將舊的 ClassPool 丟棄。 如果舊的 ClassPool 被垃圾回收掉僧凤,那么包含在 ClassPool 中的 CtClass 對象也會被回收畜侦。要創(chuàng)建一個新的 ClassPool,參見以下代碼:
ClassPool cp = new ClassPool(true);
// if needed, append an extra search path by appendClassPath()
這段代碼創(chuàng)建了一個 ClassPool 對象躯保,它的行為與 ClassPool.getDefault() 類似旋膳。 請注意,ClassPool.getDefault() 是為了方便而提供的單例工廠方法途事,它保留了一個ClassPool
的單例并重用它验懊。getDefault() 返回的 ClassPool 對象并沒有特殊之處。
注意:new ClassPool(true) 構造一個 ClassPool 對象尸变,并附加了系統(tǒng)搜索路徑义图。
調用此構造函數(shù)等效于以下代碼:
ClassPool cp = new ClassPool();
cp.appendSystemPath(); // or append another path by appendClassPath()
級聯(lián)的 ClassPools
如果程序正在 Web 應用程序服務器上運行,則可能需要創(chuàng)建多個 ClassPool 實例; 應為每一個 ClassLoader 創(chuàng)建一個 ClassPool 的實例召烂。 程序應該通過 ClassPool 的構造函數(shù)碱工,而不是調用 getDefault() 來創(chuàng)建一個 ClassPool 對象。
多個 ClassPool 對象可以像 java.lang.ClassLoader 一樣級聯(lián)骑晶。 例如痛垛,
ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");
如果調用 child.get(),子 ClassPool 首先委托給父 ClassPool桶蛔。如果父 ClassPool 找不到類文件匙头,那么子 ClassPool 會嘗試在 ./classes 目錄下查找類文件。
如果 child.childFirstLookup 返回 true仔雷,那么子類 ClassPool 會在委托給父 ClassPool 之前嘗試查找類文件蹂析。 例如:
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.
拷貝一個已經存在的類來定義一個新的類
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.setName("Pair");
這個程序首先獲得類 Point 的 CtClass 對象舔示。然后它調用 setName() 將這個 CtClass 對象的名稱設置為 Pair。在這個調用之后电抚,這個 CtClass 對象所代表的類的名稱 Point 被修改為 Pair惕稻。類定義的其他部分不會改變。
注意:CtClass 中的 setName() 改變了 ClassPool 中的記錄蝙叛。從實現(xiàn)的角度來看俺祠,一個 ClassPool 對象是一個 CtClass 對象的哈希表。setName() 更改了與哈希表中的 CtClass 對象相關聯(lián)的 Key借帘。Key 從原始類名更改為新類名蜘渣。
因此,如果后續(xù)在 ClassPool 對象上再次調用 get("Point")肺然,則它不會返回變量 cc 所指的 CtClass 對象蔫缸。 而是再次讀取類文件 Point.class,并為類 Point 構造一個新的 CtClass 對象际起。 因為與 Point 相關聯(lián)的 CtClass 對象不再存在拾碌。示例:
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 指向 CtClass 的同一個實例,而 cc3 不是街望。 注意校翔,在執(zhí)行 cc.setName("Pair") 之后,cc 和 cc1 引用的 CtClass 對象都表示 Pair 類它匕。
ClassPool 對象用于維護類和 CtClass 對象之間的一對一映射關系展融。 為了保證程序的一致性窖认,Javassist 不允許用兩個不同的 CtClass 對象來表示同一個類豫柬,除非創(chuàng)建了兩個獨立的 ClassPool。
如果你有兩個 ClassPool 對象扑浸,那么你可以從每個 ClassPool 中烧给,獲取一個表示相同類文件的不同的 CtClass 對象。 你可以修改這些 CtClass 對象來生成不同版本的類喝噪。
通過重命名凍結的類來生成新的類
一旦一個 CtClass 對象被 writeFile() 或 toBytecode() 轉換為一個類文件础嫡,Javassist 會拒絕對該 CtClass 對象的進一步修改。因此酝惧,在表示 Point 類的 CtClass 對象被轉換為類文件之后榴鼎,你不能將 Pair 類定義為 Point 的副本,因為在 Point 上執(zhí)行 setName() 會被拒絕晚唇。 以下代碼段是錯誤的:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
cc.setName("Pair"); // wrong since writeFile() has been called.
為了避免這種限制巫财,你應該在 ClassPool 中調用 getAndRename() 方法。 例如:
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.get("Point");
cc.writeFile();
CtClass cc2 = pool.getAndRename("Point", "Pair");
如果調用 getAndRename()哩陕,ClassPool 首先讀取 Point.class 來創(chuàng)建一個新的表示 Point 類的 CtClass 對象平项。 而且赫舒,它會在這個 CtClass 被記錄到哈希表之前,將 CtClass 對象重命名為 Pair闽瓢。因此接癌,getAndRename() 可以在表示 Point 類的 CtClass 對象上調用 writeFile() 或 toBytecode() 后執(zhí)行。
3. 類加載器 (Class Loader)
如果事先知道要修改哪些類扣讼,修改類的最簡單方法如下:
- 調用 ClassPool.get() 獲取 CtClass 對象缺猛,
- 修改 CtClass
- 調用 CtClass 對象的 writeFile() 或者 toBytecode() 獲得修改過的類文件。
如果在加載時椭符,可以確定是否要修改某個類枯夜,用戶必須使 Javassist 與類加載器協(xié)作,以便在加載時修改字節(jié)碼艰山。用戶可以定義自己的類加載器湖雹,也可以使用 Javassist 提供的類加載器。
3.1 CtClass.toClass()
CtClass 的 toClass() 方法請求當前線程的上下文類加載器曙搬,加載 CtClass 對象所表示的類摔吏。要調用此方法,調用者必須具有相關的權限; 否則纵装,可能會拋出 SecurityException征讲。示例:
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()。然后它構造一個修改過的 Hello 類的實例橡娄,并在該實例上調用 say() 诗箍。
注意:上面的程序要正常運行,Hello 類在調用 toClass() 之前不能被加載挽唉。 如果 JVM 在 toClass() 調用之前加載了原始的 Hello 類滤祖,后續(xù)加載修改的 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");
:
}
那么,原始的 Hello 類在 main 的第一行被加載塑顺,toClass() 調用會拋出一個異常汤求,因為類加載器不能同時加載兩個不同版本的 Hello 類。
如果程序在某些應用程序服務器(如JBoss和Tomcat)上運行严拒,toClass() 使用的上下文類加載器可能是不合適的扬绪。在這種情況下,你會看到一個意想不到的 ClassCastException裤唠。為了避免這個異常挤牛,必須給 toClass() 指定一個合適的類加載器。 例如巧骚,如果 'bean' 是你的會話 bean 對象赊颠,那么下面的代碼:
CtClass cc = ...;
Class c = cc.toClass(bean.getClass().getClassLoader());
可以工作格二。你應該給 toClass() 傳遞加載了你的程序的類加載器(上例中,bean
對象的類)竣蹦。
toClass() 是為了簡便而提供的方法顶猜。如果你需要更復雜的功能,你應該編寫自己的類加載器痘括。
3.2 Java的類加載機制
在Java中长窄,多個類加載器可以共存,每個類加載器創(chuàng)建自己的名稱空間纲菌。不同的類加載器可以加載具有相同類名的不同類文件挠日。加載的兩個類被視為不同的類。此功能使我們能夠在單個 JVM 上運行多個應用程序翰舌,即使這些程序包含具有相同名稱的不同的類嚣潜。
注意:JVM 不允許動態(tài)重新加載類。一旦類加載器加載了一個類椅贱,它不能在運行時重新加載該類的修改版本懂算。因此,在JVM 加載類之后庇麦,你不能更改類的定義计技。但是,JPDA(Java平臺調試器架構)提供有限的重新加載類的能力山橄。參見3.6節(jié)垮媒。
如果相同的類文件由兩個不同的類加載器加載,則 JVM 會創(chuàng)建兩個具有相同名稱和定義的不同的類航棱。由于兩個類不相同睡雇,一個類的實例不能被分配給另一個類的變量。兩個類之間的轉換操作將失敗并拋出一個 ClassCastException丧诺。
例如入桂,下面的代碼會拋出異常:
MyClassLoader myLoader = new MyClassLoader();
Class clazz = myLoader.loadClass("Box");
Object obj = clazz.newInstance();
Box b = (Box)obj; // this always throws ClassCastException.
Box 類由兩個類加載器加載奄薇。假設類加載器 CL 加載包含此代碼片段的類驳阎。因為這段代碼引用了 MyClassLoader,Class馁蒂,Object 和 Box呵晚,CL 也加載這些類(除非它委托給另一個類加載器)。 因此沫屡,變量 b 的類型是 CL 加載的 Box 類饵隙。 另一方面, myLoader 也加載了 Box class沮脖。 對象 obj 是由 myLoader 加載的 Box 類的一個實例金矛。 因此芯急,最后一個語句總是拋出 ClassCastException ,因為 obj 的類是一個不同的 Box 類的類型驶俊,而不是用作變量 b 的類型娶耍。
多個類加載器形成一個樹型結構。 除引導類加載器之外的每個類加載器饼酿,都有一個父類加載器榕酒,它通常加載該子類加載器的類。 因為加載類的請求可以沿類加載器的這個層次委派故俐,所以即使你沒有請求加載一個類想鹰,它也可能被加載。因此药版,已經請求加載類 C 的類加載器可以不同于實際加載類 C 的加載器辑舷。為了區(qū)分,我們將前加載器稱為 C 的發(fā)起者槽片,將后加載器稱為 C 的實際加載器 惩妇。
此外,如果請求加載類 C(C的發(fā)起者)的類加載器 CL 委托給父類加載器 PL筐乳,則類加載器 CL 不會加載類 C 引用的任何類歌殃。因為 CL 不是那些類的發(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 的啟動器和實際加載器都是 L波材。由于 Window 的定義引用了 Box,JVM 將請求 L 加載 Box身隐。 這里廷区,假設 L 將該任務委托給父類加載器 PL。Box 的啟動器是 L贾铝,但真正的加載器是 PL隙轻。 在這種情況下,Point 的啟動器不是 L 而是 PL垢揩,因為它與 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。 你必須避免有兩個類加載器兩次加載同一個類贮勃。兩個加載器之一必須委托給另一個牢酵。
當 Point 加載時,如果 L 不委托給 PL衙猪,widthIs() 就會拋出一個 ClassCastException 異常馍乙。因為 Box 的實際加載器是 PL,在 Box 中引用的 Point 也由 PL 加載垫释。 getSize() 的結果值是由 PL 加載的 Point丝格,widthIs() 中的變量 p 是由 L 加載的 Point。JVM 認為它們是不同的類型棵譬,因此它會拋出類型不匹配的異常显蝌。
這種設計有點不方便,但也是必須的订咸。
Point p = box.getSize();
如果上面的語句沒有拋出異常曼尊,那么 Window 的程序員可以破壞 Point 對象的封裝。 例如脏嚷,字段 x 在 PL 中加載的 Point 中是私有的骆撇。 然而,如果 L 加載具有以下定義的 Point父叙,則 Window 類可以直接訪問 x 的值:
public class Point {
public int x, y; // not private
public int getX() { return x; }
:
}
有關 Java 類加載器的更多詳細信息神郊,可以參看以下文章:
Sheng Liang 和 Gilad Bracha,“Dynamic Class Loading in the Java Virtual Machine”趾唱,* ACM OOPSLA'98 *涌乳,pp.36-44,1998。
3.3 使用 javassist.Loader
Javassit 提供一個類加載器 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蒸辆。然后再加載修改的類,并創(chuàng)建新的 test.Rectangle 類的實例乔煞。
如果用戶希望在加載時按需修改類吁朦,則可以向 javassist.Loader 添加事件監(jiān)聽器。當類加載器加載類時會通知監(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;
}
當事件監(jiān)聽器通過 addTranslator() 添加到 javassist.Loader 對象時,start() 方法會被調用雄右。在 javassist.Loader 加載類之前空骚,會調用 onLoad() 方法纺讲。可以在 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() 不必調用 toBytecode() 或 writeFile(),因為 javassist.Loader 會調用這些方法來獲取類文件智厌。
要使用 MyTranslator 對象運行一個應用程序類 MyApp诲泌,主類代碼如下:
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 和其他應用程序類會被 MyTranslator 監(jiān)聽。
注意铣鹏,MyApp 不能訪問 loader 類敷扫,如 Main2,MyTranslator 和 ClassPool诚卸,因為它們是由不同的加載器加載的葵第。 應用程序類由 javassist.Loader 加載,而加載器類(例如 Main2)由默認的 Java 類加載器加載合溺。
javassist.Loader 以不同的順序從 java.lang.ClassLoader 中搜索類卒密。ClassLoader 首先將加載操作委托給父類加載器,只有當父類加載器無法找到它們時才嘗試自己加載類棠赛。另一方面栅受,javassist.Loader 嘗試在委托給父類加載器之前加載類。它僅在以下情況下進行委派:
- 在 ClassPool 對象上調用 get() 找不到這個類恭朗;
- 這些類已經通過 delegateLoadingOf() 來指定由父類加載器加載屏镊。
此搜索順序允許 Javassist 加載修改過的類。但是痰腮,如果找不到修改的類而芥,它將委托父類加載器來加載。一旦一個類被父類加載器加載膀值,那個類中引用的其他類也將被父類加載器加載棍丐,因此它們是沒有被修改的。 回想一下沧踏,C 類引用的所有類都由 C 的實際加載器加載的歌逢。如果你的程序無法加載修改的類,你應該確保所有使用該類的類都是由 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();
}
}
}
MyApp 類是一個應用程序秘案。 要執(zhí)行此程序,首先將類文件放在 ./class 目錄下,它不能包含在類搜索路徑中阱高。 否則赚导,MyApp.class 將由默認系統(tǒng)類加載器加載,它是 SampleLoader 的父加載器赤惊。目錄名 ./class 由構造函數(shù)中的 insertClassPath() 指定吼旧。然后運行:
% java SampleLoader
類加載器會加載類 MyApp (./class/MyApp.class),并使用命令行參數(shù)調用 MyApp.main()未舟。
這是使用 Javassist 的最簡單的方法圈暗。 但是,如果你編寫一個更復雜的類加載器裕膀,你可能需要更詳細地了解 Java 的類加載機制员串。 例如,上面的程序將 MyApp 類放在與 SampleLoader 類不同的命名空間中魂角,因為這兩個類由不同的類裝載器加載昵济。 因此,MyApp 類不能直接訪問類 SampleLoader野揪。
3.5 修改系統(tǒng)的類
像 java.lang.String 這樣的系統(tǒng)類只能被系統(tǒng)類加載器加載访忿。因此,上面的 SampleLoader 或 javassist.Loader 在加載時不能修改系統(tǒng)類斯稳。系統(tǒng)類必須被靜態(tài)地修改海铆。下面的程序向 java.lang.String 添加一個新字段 hiddenValue:
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(".");
這段程序生成一個新文件 ./java/lang/String.class
可以使用 MyApp 這樣測試修改過的 String 類:
% java -Xbootclasspath/p:. MyApp arg1 arg2...
MyApp 的定義如下:
public class MyApp {
public static void main(String[] args) throws Exception {
System.out.println(String.class.getField("hiddenValue").getName());
}
}
如果修改過的 String 類被加載,MyApp 會打印出 hiddenValue挣惰。
注意:如果應用使用此技術來覆蓋 rt.jar 中的系統(tǒng)類卧斟,那么部署這個應用會違反 Java 2 運行時二進制代碼許可協(xié)議。
3.6 在運行時重新加載類
如果 JVM 在啟用 JPDA(Java平臺調試器體系結構)的情況下啟動憎茂,那么類可以被動態(tài)地重新加載珍语。在 JVM 加載類之后,舊版本的類可以被卸載竖幔,新版本可以再次重新加載板乙。也就是說,該類的定義可以在運行時動態(tài)被修改拳氢。然而募逞,新的類定義必須與舊的類定義有些兼容。JVM 不允許兩個版本之間的模式更改。它們必須具有相同的方法和字段。
Javassist 提供了一個方便的類冈欢,用于在運行時重新加載類。更多相關信息划滋,請參閱javassist.tools.HotSwapper 的 API 文檔巩螃。