Javassist 使用指南(一)
本文譯自: Javassist Tutorial-1
原作者: Shigeru Chiba
完成時間:2016年11月
1. 讀寫字節碼
我們知道 Java 字節碼以二進制的形式存儲在 class 文件中,每一個 class 文件包含一個 Java 類或接口。Javaassist 就是一個用來處理 Java 字節碼的類庫。
在 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 對象使用的是默認系統的類搜索路徑。
從實現的角度來看,ClassPool 是一個存儲 CtClass 的 Hash 表,類的名稱作為 Hash 表的 key。ClassPool 的 get() 函數用于從 Hash 表中查找 key 對應的 CtClass 對象。如果沒有找到,get() 函數會創建并返回一個新的 CtClass 對象,這個新對象會保存在 Hash 表中。
從 ClassPool 中獲取的 CtClass 是可以被修改的(稍后會討論細節)。
在上面的例子中,test.Rectangle 的父類被設置為 test.Point。調用 writeFile() 后,這項修改會被寫入原始類文件。writeFile() 會將 CtClass 對象轉換成類文件并寫到本地磁盤。也可以使用 toBytecode() 函數來獲取修改過的字節碼:
byte[] b = cc.toBytecode();你也可以通過 toClass() 函數直接將 CtClass 轉換成 Class 對象:
Class clazz = cc.toClass();toClass() 請求當前線程的 ClassLoader 加載 CtClass 所代表的類文件。它返回此類文件的 java.lang.Class 對象,更多細節,請參考下面的章節。
定義新類
使用 ClassPool 的 makeClass() 方法可以定義一個新類。
ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.makeClass("Point");這段代碼定義了一個空的 Point 類。Point 類的成員方法可以通過 CtNewMethod 類的工廠方法來創建,然后使用 CtClass 的 addMethod() 方法將其添加到 Point 中。
使用 ClassPool 中的 makeInterface() 方法可以創建新接口。接口中的方法可以使用 CtNewMethod 的 abstractMethod() 方法創建。
將類凍結
如果一個 CtClass 對象通過 writeFile(), toClass(), toBytecode() 被轉換成一個類文件,此 CtClass 對象會被凍結起來,不允許再修改。因為一個類只能被 JVM 加載一次。
但是,一個冷凍的 CtClass 也可以被解凍,例如:
CtClasss cc = ...;: cc.writeFile(); cc.defrost(); cc.setSuperclass(...); // 因為類已經被解凍,所以這里可以調用成功調用 defrost() 之后,此 CtClass 對象又可以被修改了。
如果 ClassPool.doPruning 被設置為 true,Javassist 在凍結 CtClass 時,會修剪 CtClass 的數據結構。為了減少內存的消耗,修剪操作會丟棄 CtClass 對象中不必要的屬性。例如,Code_attribute 結構會被丟棄。一個 CtClass 對象被修改之后,方法的字節碼是不可訪問的,但是方法名稱、方法簽名、注解信息可以被訪問。修剪過的 CtClass 對象不能再次被解凍。ClassPool.doPruning 的默認值為 false。
stopPruning() 可以用來駁回修剪操作。
CtClasss cc = ...; cc.stopPruning(true);: cc.writeFile(); // 轉換成一個 class 文件 // cc is not pruned.這個 CtClass 沒有被修剪,所以在 writeFile() 之后,可以被解凍。
注意:調試的時候,你可能臨時需要停止修剪和凍結,然后保存一個修改過的類文件到磁盤,debugWriteFile() 方法正是為此準備的。它停止修剪,然后寫類文件,然后解凍并再次打開修剪(如果開始時修養是打開的)。
類搜索路徑
通過 ClassPool.getDefault() 獲取的 ClassPool 使用 JVM 的類搜索路徑。如果程序運行在 JBoss 或者 Tomcat 等 Web 服務器上,ClassPool 可能無法找到用戶的類,因為 Web 服務器使用多個類加載器作為系統類加載器。在這種情況下,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 數組給 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() 可以用于優化此搜索。由 makeClass() 構造的 CtClass 保存在 ClassPool 對象中,從而使得類文件不會再被讀取。
用戶可以通過實現 ClassPath 接口來擴展類加載路徑,然后調用 ClassPool 的 insertClassPath() 方法將路徑添加進來。這種技術主要用于將非標準資源添加到類搜索路徑中。
2. ClassPool
ClassPool 是 CtClass 對象的容器。因為編譯器在編譯引用 CtClass 代表的 Java 類的源代碼時,可能會引用 CtClass 對象,所以一旦一個 CtClass 被創建,它就被保存在 ClassPool 中.
例如,一個 CtClass 類代表 Point 類,并給 CtClass 添加 getter() 方法。然后,程序嘗試編譯一段代碼,代碼中包含了 Point 的 getter() 調用,然后將這段代碼添加了另一個類 Line 中,如果代表 Point 的 CtClass 丟失,編譯器就無法編譯 Line 中的 Point.getter() 方法。注:原來的 Point 類中無 getter() 方法。因此,為了能夠正確編譯這個方法調用,ClassPool 必須在程序執行期間包含所有的 CtClass 實例。
避免內存溢出
如果 CtClass 對象的數量變得非常大(這種情況很少發生,因為 Javassist 試圖以各種方式減少內存消耗),ClassPool 可能會導致巨大的內存消耗。 為了避免此問題,可以從 ClassPool 中顯式刪除不必要的 CtClass 對象。 如果對 CtClass 對象調用 detach(),那么該 CtClass 對象將被從 ClassPool 中刪除。 例如:
CtClass cc = ... ; cc.writeFile(); cc.detach();在調用 detach() 之后,就不能調用這個 CtClass 對象的任何方法了。但是如果你調用 ClassPool 的 get() 方法,ClassPool 會再次讀取這個類文件,創建一個新的 CtClass 對象。
另一個辦法是用新的 ClassPool 替換舊的 ClassPool,并將舊的 ClassPool 丟棄。 如果舊的 ClassPool 被垃圾回收掉,那么包含在 ClassPool 中的 CtClass 對象也會被回收。要創建一個新的 ClassPool,參見以下代碼:
ClassPool cp = new ClassPool(true); // if needed, append an extra search path by appendClassPath()這段代碼創建了一個 ClassPool 對象,它的行為與 ClassPool.getDefault() 類似。 請注意,ClassPool.getDefault() 是為了方便而提供的單例工廠方法,它保留了一個ClassPool的單例并重用它。getDefault() 返回的 ClassPool 對象并沒有特殊之處。
注意:new ClassPool(true) 構造一個 ClassPool 對象,并附加了系統搜索路徑。
調用此構造函數等效于以下代碼:
級聯的 ClassPools
如果程序正在 Web 應用程序服務器上運行,則可能需要創建多個 ClassPool 實例; 應為每一個 ClassLoader 創建一個 ClassPool 的實例。 程序應該通過 ClassPool 的構造函數,而不是調用 getDefault() 來創建一個 ClassPool 對象。
多個 ClassPool 對象可以像 java.lang.ClassLoader 一樣級聯。 例如,
如果調用 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 中的記錄。從實現的角度來看,一個 ClassPool 對象是一個 CtClass 對象的哈希表。setName() 更改了與哈希表中的 CtClass 對象相關聯的 Key。Key 從原始類名更改為新類名。
因此,如果后續在 ClassPool 對象上再次調用 get("Point"),則它不會返回變量 cc 所指的 CtClass 對象。 而是再次讀取類文件 Point.class,并為類 Point 構造一個新的 CtClass 對象。 因為與 Point 相關聯的 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 不是。 注意,在執行 cc.setName("Pair") 之后,cc 和 cc1 引用的 CtClass 對象都表示 Pair 類。
ClassPool 對象用于維護類和 CtClass 對象之間的一對一映射關系。 為了保證程序的一致性,Javassist 不允許用兩個不同的 CtClass 對象來表示同一個類,除非創建了兩個獨立的 ClassPool。
如果你有兩個 ClassPool 對象,那么你可以從每個 ClassPool 中,獲取一個表示相同類文件的不同的 CtClass 對象。 你可以修改這些 CtClass 對象來生成不同版本的類。
通過重命名凍結的類來生成新的類
一旦一個 CtClass 對象被 writeFile() 或 toBytecode() 轉換為一個類文件,Javassist 會拒絕對該 CtClass 對象的進一步修改。因此,在表示 Point 類的 CtClass 對象被轉換為類文件之后,你不能將 Pair 類定義為 Point 的副本,因為在 Point 上執行 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 來創建一個新的表示 Point 類的 CtClass 對象。 而且,它會在這個 CtClass 被記錄到哈希表之前,將 CtClass 對象重命名為 Pair。因此,getAndRename() 可以在表示 Point 類的 CtClass 對象上調用 writeFile() 或 toBytecode() 后執行。
3. 類加載器 (Class Loader)
如果事先知道要修改哪些類,修改類的最簡單方法如下:
如果在加載時,可以確定是否要修改某個類,用戶必須使 Javassist 與類加載器協作,以便在加載時修改字節碼。用戶可以定義自己的類加載器,也可以使用 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 類,后續加載修改的 Hello 類將會失敗(LinkageError 拋出)。
例如,如果 Test 中的 main() 是這樣的:
那么,原始的 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中,多個類加載器可以共存,每個類加載器創建自己的名稱空間。不同的類加載器可以加載具有相同類名的不同類文件。加載的兩個類被視為不同的類。此功能使我們能夠在單個 JVM 上運行多個應用程序,即使這些程序包含具有相同名稱的不同的類。
注意:JVM 不允許動態重新加載類。一旦類加載器加載了一個類,它不能在運行時重新加載該類的修改版本。因此,在JVM 加載類之后,你不能更改類的定義。但是,JPDA(Java平臺調試器架構)提供有限的重新加載類的能力。參見3.6節。
如果相同的類文件由兩個不同的類加載器加載,則 JVM 會創建兩個具有相同名稱和定義的不同的類。由于兩個類不相同,一個類的實例不能被分配給另一個類的變量。兩個類之間的轉換操作將失敗并拋出一個 ClassCastException。
例如,下面的代碼會拋出異常:
Box 類由兩個類加載器加載。假設類加載器 CL 加載包含此代碼片段的類。因為這段代碼引用了 MyClassLoader,Class,Object 和 Box,CL 也加載這些類(除非它委托給另一個類加載器)。 因此,變量 b 的類型是 CL 加載的 Box 類。 另一方面, myLoader 也加載了 Box class。 對象 obj 是由 myLoader 加載的 Box 類的一個實例。 因此,最后一個語句總是拋出 ClassCastException ,因為 obj 的類是一個不同的 Box 類的類型,而不是用作變量 b 的類型。
多個類加載器形成一個樹型結構。 除引導類加載器之外的每個類加載器,都有一個父類加載器,它通常加載該子類加載器的類。 因為加載類的請求可以沿類加載器的這個層次委派,所以即使你沒有請求加載一個類,它也可能被加載。因此,已經請求加載類 C 的類加載器可以不同于實際加載類 C 的加載器。為了區分,我們將前加載器稱為 C 的發起者,將后加載器稱為 C 的實際加載器 。
此外,如果請求加載類 C(C的發起者)的類加載器 CL 委托給父類加載器 PL,則類加載器 CL 不會加載類 C 引用的任何類。因為 CL 不是那些類的發起者。 相反,父類加載器 PL 成為它們的啟動器,并且加載它們。
請參考下面的例子來理解:
public class Point { // loaded by PLprivate int x, y;public int getX() { return x; }: }public class Box { // the initiator is L but the real loader is PLprivate Point upperLeft, size;public int getBaseX() { return upperLeft.x; }: }public class Window { // loaded by a class loader Lprivate 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();
}
}
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; }
:
}
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);
}
}
public interface Translator {
public void start(ClassPool pool)
throws NotFoundException, CannotCompileException;
public void onLoad(ClassPool pool, String classname)
throws NotFoundException, CannotCompileException;
}
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);
}
}
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);
}
}
% java Main2 arg1 arg2...
類 MyApp 和其他應用程序類會被 MyTranslator 監聽。注意,MyApp 不能訪問 loader 類,如 Main2,MyTranslator 和 ClassPool,因為它們是由不同的加載器加載的。 應用程序類由 javassist.Loader 加載,而加載器類(例如 Main2)由默認的 Java 類加載器加載。javassist.Loader 以不同的順序從 java.lang.ClassLoader 中搜索類。ClassLoader 首先將加載操作委托給父類加載器,只有當父類加載器無法找到它們時才嘗試自己加載類。另一方面,javassist.Loader 嘗試在委托給父類加載器之前加載類。它僅在以下情況下進行委派:1. 在 ClassPool 對象上調用 get() 找不到這個類; 2. 這些類已經通過 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 });
}
}
MyApp 類是一個應用程序。 要執行此程序,首先將類文件放在 ./class 目錄下,它不能包含在類搜索路徑中。 否則,MyApp.class 將由默認系統類加載器加載,它是 SampleLoader 的父加載器。目錄名 ./class 由構造函數中的 insertClassPath() 指定。然后運行:``% java SampleLoader``類加載器會加載類 MyApp (./class/MyApp.class),并使用命令行參數調用 MyApp.main()。這是使用 Javassist 的最簡單的方法。 但是,如果你編寫一個更復雜的類加載器,你可能需要更詳細地了解 Java 的類加載機制。 例如,上面的程序將 MyApp 類放在與 SampleLoader 類不同的命名空間中,因為這兩個類由不同的類裝載器加載。 因此,MyApp 類不能直接訪問類 SampleLoader。### 3.5 修改系統的類 像 java.lang.String 這樣的系統類只能被系統類加載器加載。因此,上面的 SampleLoader 或 javassist.Loader 在加載時不能修改系統類。系統類必須被靜態地修改。下面的程序向 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 -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());
}
}
作者:二胡
鏈接:https://www.jianshu.com/p/43424242846b
來源:簡書
總結
以上是生活随笔為你收集整理的Javassist 使用指南(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java动态编程初探——Javassis
- 下一篇: Java深度历险(五)——Java泛型