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 對象,並附加了系統搜索路徑。
調用此構造函數等效於以下代碼:
-
ClassPool cp = new ClassPool();
-
cp.appendSystemPath(); // or append another path by appendClassPath()
級聯的 ClassPools
如果程序正在 Web 應用程序服務器上運行,則可能需要創建多個 ClassPool 實例; 應為每一個 ClassLoader 創建一個 ClassPool 的實例。 程序應該通過 ClassPool 的構造函數,而不是調用 getDefault() 來創建一個 ClassPool 對象。
多個 ClassPool 對象可以像 java.lang.ClassLoader 一樣級聯。 例如,
-
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 中的記錄。從實現的角度來看,一個 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)
如果事先知道要修改哪些類,修改類的最簡單方法如下:
- 調用 ClassPool.get() 獲取 CtClass 對象,
- 修改 CtClass
- 調用 CtClass 對象的 writeFile() 或者 toBytecode() 獲得修改過的類文件。
如果在加載時,可以確定是否要修改某個類,用戶必須使 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() 是這樣的:
-
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中,多個類加載器可以共存,每個類加載器創建自己的名稱空間。不同的類加載器可以加載具有相同類名的不同類文件。加載的兩個類被視為不同的類。此功能使我們能夠在單個 JVM 上運行多個應用程序,即使這些程序包含具有相同名稱的不同的類。
注意:JVM 不允許動態重新加載類。一旦類加載器加載了一個類,它不能在運行時重新加載該類的修改版本。因此,在JVM 加載類之后,你不能更改類的定義。但是,JPDA(Java平台調試器架構)提供有限的重新加載類的能力。參見3.6節。
如果相同的類文件由兩個不同的類加載器加載,則 JVM 會創建兩個具有相同名稱和定義的不同的類。由於兩個類不相同,一個類的實例不能被分配給另一個類的變量。兩個類之間的轉換操作將失敗並拋出一個 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 的加載器。為了區分,我們將前加載器稱為 C 的發起者,將后加載器稱為 C 的實際加載器 。
此外,如果請求加載類 C(C的發起者)的類加載器 CL 委托給父類加載器 PL,則類加載器 CL 不會加載類 C 引用的任何類。因為 CL 不是那些類的發起者。 相反,父類加載器 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();
-
}
-
}
現在,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。然后再加載修改的類,並創建新的 test.Rectangle 類的實例。
如果用戶希望在加載時按需修改類,則可以向 javassist.Loader 添加事件監聽器。當類加載器加載類時會通知監聽器。事件監聽器類必須實現以下接口:
-
public interface Translator {
-
public void start (ClassPool pool)
-
throws NotFoundException, CannotCompileException;
-
public void onLoad (ClassPool pool, String classname)
-
throws NotFoundException, CannotCompileException;
-
}
當事件監聽器通過 addTranslator() 添加到 javassist.Loader 對象時,start() 方法會被調用。在 javassist.Loader 加載類之前,會調用 onLoad() 方法。可以在 onLoad() 方法中修改被加載的類的定義。
例如,下面的事件監聽器在類加載之前,將所有類更改為 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);
-
}
-
}
執行下面的命令來運行程序:
% java Main2 arg1 arg2...
類 MyApp 和其他應用程序類會被 MyTranslator 監聽。
注意,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 類是一個應用程序。 要執行此程序,首先將類文件放在 ./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/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 中的系統類,那么部署這個應用會違反 Java 2 運行時二進制代碼許可協議。
3.6 在運行時重新加載類
如果 JVM 在啟用 JPDA(Java平台調試器體系結構)的情況下啟動,那么類可以被動態地重新加載。在 JVM 加載類之后,舊版本的類可以被卸載,新版本可以再次重新加載。也就是說,該類的定義可以在運行時動態被修改。然而,新的類定義必須與舊的類定義有些兼容。JVM 不允許兩個版本之間的模式更改。它們必須具有相同的方法和字段。
Javassist 提供了一個方便的類,用於在運行時重新加載類。更多相關信息,請參閱javassist.tools.HotSwapper 的 API 文檔。
4. 自省和自定制 (Introspection and customization)
CtClass 提供了自省的方法。Javassist 的自省能力與 Java 反射 API 兼容。 CtClass 提供了 getName(),getSuperclass(),getMethods() 等方法來獲取類的信息,也提供了修改類定義的方法(添加字段,添加構造函數、添加方法),同時也可以對方法體的語句進行檢測。
方法由 CtMethod 對象表示。CtMethod 提供了幾個函數來修改方法的定義。
注意,如果一個方法繼承自一個超類,那么表示繼承方法的 CtMethod 對象,同樣也表示該超類中聲明的方法。
例如,如果類 Point 聲明方法 move() , Point 的子類 ColorPoint 不覆蓋 move() ,那么在 Point 中聲明的 move 和 ColorPoint 的 move 具有相同的 CtMethod。 如果由這個 CtMethod 對象表示的方法定義被修改,那么修改將反映在這兩種方法上。 如果你只想修改 ColorPoint 中的 move() 方法,你首先必須在 Point 中加入 CtMethod 對象的副本 move() 。CtMethod 對象的副本可以通過 CtNewMethod.copy() 獲得。
Javassist 不允許刪除方法或字段,但它允許更改名稱。所以,如果一個方法是沒有必要的,可以通過調用 CtMethod 的 setName() 和 setModifiers() 中將其改為一個私有方法。
Javassist 不允許向現有方法添加額外的參數。你可以通過新建一個方法達到同樣的效果。 例如,如果你想為一個方法添加一個額外的 int 參數 newZ 到 Point 類,
void move(int newX, int newY) { x = newX; y = newY; }
你可以添加一個這樣的方法到 Point 類:
-
void move(int newX, int newY, int newZ) {
-
// do what you want with newZ.
-
move(newX, newY);
-
}
Javassist 還提供了用於直接編輯原始類文件的低級API。
例如,CtClass 中的 getClassFile() 返回一個表示類文件的 ClassFile 對象。CtMethod 的 getMethodInfo() 方法返回一個 MethodInfo 對象,表示類文件中的 method_info 結構。 低級API使用 Java 虛擬機規范中的詞匯表。 用戶必須具有類文件和字節碼的知識。有關更多詳細信息,請參考 javassist.bytecode 包。
由 Javassist 修改的類文件只有在使用以 $ 開頭的特殊標識符時才需要 javassist.runtime 包來提供運行時支持。接下來的內容會討論這些特殊標識符。在沒有這些特殊標識符的情況下,在運行時修改類文件不需要 javassist.runtime 包或任何其他 Javassist 包。有關更多詳細信息,請參閱 javassist.runtime 包的API文檔。
4.1 在方法體的開始/結尾處添加代碼
CtMethod 和 CtConstructor 提供了 insertBefore(),insertAfter() 和 addCatch() 方法。 它們可以將用 Java 編寫的代碼片段插入到現有方法中。Javassist 包括一個用於處理源代碼的簡單編譯器,它接收用 Java 編寫的源代碼,並將其編譯成 Java 字節碼,並內聯方法體中。
也可以按行號來插入代碼段(如果行號表包含在類文件中)。向 CtMethod 和 CtConstructor 中的 insertAt() 方法提供源代碼和原始類定義中的源文件的行號,就可以將編譯后的代碼插入到指定行號位置。
方法 insertBefore() ,insertAfter(),addCatch() 和 insertAt() 接收一個表示語句或語句塊的 String 對象。一個語句是一個單一的控制結構,比如 if 和 while 或者以分號結尾的表達式。語句塊是一組用大括號 {} 包圍的語句。因此,以下每行都是有效語句或塊的示例:
-
System. out.println( "Hello");
-
{ System. out.println( "Hello"); }
-
if (i < 0) { i = -i; }
語句和語句塊可以引用字段和方法。如果使用 -g 選項(在類文件中包含局部變量屬性)編譯該方法,則它們還可以引用方法的參數。 否則,它們必須通過特殊變量 $0,$1,$2,... 來訪問方法參數,下面會討論。不允許訪問在方法中聲明的局部變量,盡管在塊中聲明一個新的局部變量是允許的。但是,insertAt() 允許語句和塊訪問局部變量,前提是這些變量在指定的行號處可用,並且目標方法是使用 -g 選項編譯的。
傳遞給方法 insertBefore() ,insertAfter() ,addCatch() 和 insertAt() 的 String 對象是由Javassist 的編譯器編譯的。 由於編譯器支持語言擴展,以 $ 開頭的幾個標識符有特殊的含義:
符號 | 含義 |
---|---|
$0 , $1 , $2 , ... |
this and 方法的參數 |
$args |
方法參數數組.它的類型為 Object[] |
$$ |
所有實參。例如, m($$) 等價於 m($1,$2, ...) |
$cflow( ...) |
cflow 變量 |
$r |
返回結果的類型,用於強制類型轉換 |
$w |
包裝器類型,用於強制類型轉換 |
$_ |
返回值 |
$sig |
類型為 java.lang.Class 的參數類型數組 |
$type |
一個 java.lang.Class 對象,表示返回值類型 |
$class |
一個 java.lang.Class 對象,表示當前正在修改的類 |
$0, $1, $2, ...
傳遞給目標方法的參數使用 $1,$2,... 訪問,而不是原始的參數名稱。 $1 表示第一個參數,$2 表示第二個參數,以此類推。 這些變量的類型與參數類型相同。 $0 等價於 this
指針。 如果方法是靜態的,則 $0 不可用。
下面有一些使用這些特殊變量的例子。假設一個類 Point:
-
class Point {
-
int x, y;
-
void move(int dx, int dy) { x += dx; y += dy; }
-
}
要在調用方法 move() 時打印 dx 和 dy 的值,請執行以下程序:
-
ClassPool pool = ClassPool.getDefault();
-
CtClass cc = pool.get( "Point");
-
CtMethod m = cc.getDeclaredMethod( "move");
-
m.insertBefore( "{ System.out.println($1); System.out.println($2); }");
-
cc.writeFile();
請注意,傳遞給 insertBefore() 的源文本是用大括號 {} 括起來的。insertBefore() 只接受單個語句或用大括號括起來的語句塊。
修改后的類 Point 的定義是這樣的:
-
class Point {
-
int x, y;
-
void move(int dx, int dy) {
-
{ System. out.println(dx); System. out.println(dy); }
-
x += dx; y += dy;
-
}
-
}
$1
and $2
are replaced with dx
and dy
, respectively.$1
, $2
, $3
... are updatable. If a new value is assigend to one of those variables, then the value of the parameter represented by that variable is also updated.
$1 和 $2 分別替換為 dx 和 dy。
$1,$2,$3 ...是可更新的。如果這些變量被賦予新值,則由該變量表示的參數的值也將被更新。
$args
變量 $args 表示所有參數的數組。該變量的類型是 Object 類的數組。如果參數類型是原始類型(如 int),則該參數值將轉換為包裝器對象(如java.lang.Integer)以存儲在 $args 中。 因此,如果第一個參數的類型是不是原始類型,那么 $args[0] 等於 $1。注意 $args[0] 不等於 $0,因為 $0 表示 this
。
如果 Object 的數組被分配給 $args,那么該數組的每個元素都被分配給每個參數。如果參數類型是基本類型,則相應元素的類型必須是包裝類型。 在將值分配給參數之前,必須將該值從包裝器類型轉換為基本類型。
$$
變量 $$ 是所有參數列表的縮寫,用逗號分隔。 例如,如果方法 move() 的有 3 個參數,則
move($1, $2, $3)
如果 move() 不帶任何參數,則 move(
可以與其他方法一起使用。 如果你寫一個表達式:
exMove($$, context)
這個表達式等價於
exMove($1, $2, $3, context)
注意,$$ 開啟了通用符號方法調用,它通常與稍后要介紹的 $proceed 一起使用。
$cflow
$ cflow表示 控制流。此只讀變量返回特定方法的遞歸調用的深度。
假設下面所示的方法由CtMethod對象cm表示:
-
int fact(int n) {
-
if (n <= 1)
-
return n;
-
else
-
return n * fact(n - 1);
-
}
要使用 $cflow,首先聲明使用 $cflow 監視方法 fact() 的調用:
-
CtMethod cm = ...;
-
cm.useCflow( "fact");
useCflow() 的參數是 $cflow 變量的標識符。任何有效的 Java 名稱都可以用作標識符。標識符還可以包括 .
,例如,“my.Test.fact”是有效的標識符。
然后,$cflow(fact) 表示由 cm 指定的方法的遞歸調用深度。$cflow(fact) 的值在方法第一次調用時為 0,而當方法在方法中遞歸調用時為 1。 例如,
-
cm.insertBefore( "if ($cflow(fact) == 0)"
-
+ " System.out.println(\"fact \" + $1);");
翻譯方法fact(),以便它顯示參數。因為檢查了 $cflow(fact) 的值,所以如果在 fact() 中遞歸調用,則方法 fact() 不會顯示參數。
$cflow 的值是當前線程的最頂層堆棧幀下與 cm 相關聯的堆棧幀數。 $cflow 也可以不在 cm 方法中訪問。
$r
$r 表示方法的結果類型(返回類型)。它用在 cast 表達式中作 cast 轉換類型。 下面是一個典型的用法:
-
Object result = ... ;
-
$_ = ( $r)result;
如果結果類型是原始類型,則 ($r) 遵循特殊語義。 首先,如果 cast 表達式的操作數是原始類型,($r) 作為普通轉換運算符。 另一方面,如果操作數是包裝類型,($r) 將從包裝類型轉換為結果類型。 例如,如果結果類型是 int,那么 ($r) 將從 java.lang.Integer 轉換為 int。
如果結果類型為void,那么 ($r) 不轉換類型; 它什么也不做。 但是,如果操作數是對 void 方法的調用,則 ($r) 將導致 null。 例如,如果結果類型是 void,而 foo() 是一個 void 方法,那么
$_ = ($r)foo();
是一個正確的表達式。
cast 運算符 ($r) 在 return 語句中也很有用。 即使結果類型是 void,下面的 return 語句也是有效的:
return ($r)result;
這里,result是局部變量。 因為指定了 ($r),所以結果值被丟棄。此返回語句被等價於:
return;
$w
$w 表示包裝類型。它用在 cast 表達式中作 cast 轉換類型。($w) 把基本類型轉換為包裝類型。 以下代碼是一個示例:
Integer i = ($w)5;
包裝后的類型取決於 ($w) 后面表達式的類型。如果表達式的類型為 double,則包裝器類型為 java.lang.Double。
If the type of the expression following ($w)
is not a primitive type, then ($w)
does nothing.
如果下面的表達式 ($w) 的類型不是原始類型,那么($w) 什么也不做。
$_
CtMethod 中的 insertAfter() 和 CtConstructor 在方法的末尾插入編譯的代碼。傳遞給insertAfter() 的語句中,不但可以使用特殊符號如 $0,$1。也可以使用 $_ 來表示方法的結果值。
該變量的類型是方法的結果類型(返回類型)。如果結果類型為 void,那么 $_ 的類型為Object,$_ 的值為 null。
雖然由 insertAfter() 插入的編譯代碼通常在方法返回之前執行,但是當方法拋出異常時,它也可以執行。要在拋出異常時執行它,insertAfter() 的第二個參數 asFinally 必須為true。
如果拋出異常,由 insertAfter() 插入的編譯代碼將作為 finally 子句執行。$_ 的值 0 或 null。在編譯代碼的執行終止后,最初拋出的異常被重新拋出給調用者。注意,$_ 的值不會被拋給調用者,它將被丟棄。
$sig
$sig 的值是一個 java.lang.Class 對象的數組,表示聲明的形式參數類型。
$type
$type 的值是一個 java.lang.Class 對象,表示結果值的類型。 如果這是一個構造函數,此變量返回 Void.class。
$class
$class 的值是一個 java.lang.Class 對象,表示編輯的方法所在的類。 即表示 $0 的類型。
addCatch()
addCatch() 插入方法體拋出異常時執行的代碼,控制權會返回給調用者。 在插入的源代碼中,異常用 $e 表示。
例如:
-
CtMethod m = ...;
-
CtClass etype = ClassPool.getDefault(). get( "java.io.IOException");
-
m.addCatch( "{ System.out.println($e); throw $e; }", etype);
轉換成對應的 java 代碼如下:
-
try {
-
// the original method body
-
} catch (java.io.IOException e) {
-
System. out.println(e);
-
throw e;
-
}
請注意,插入的代碼片段必須以 throw 或 return 語句結束。
4.2 修改方法體
CtMethod 和 CtConstructor 提供 setBody() 來替換整個方法體。他將新的源代碼編譯成 Java 字節碼,並用它替換原方法體。 如果給定的源文本為 null,則替換后的方法體僅包含返回語句,返回零或空值,除非結果類型為 void。
在傳遞給 setBody() 的源代碼中,以 $ 開頭的標識符具有特殊含義:
符號 | 含義 |
---|---|
$0 , $1 , $2 , ... |
this and 方法的參數 |
$args |
方法參數數組.它的類型為 Object[] |
$$ |
所有實參。例如, m($$) 等價於 m($1,$2, ...) |
$cflow( ...) |
cflow 變量 |
$r |
返回結果的類型,用於強制類型轉換 |
$w |
包裝器類型,用於強制類型轉換 |
$sig |
類型為 java.lang.Class 的參數類型數組 |
$type |
一個 java.lang.Class 對象,表示返回值類型 |
$class |
一個 java.lang.Class 對象,表示當前正在修改的類 |
注意 $_ 不可用。
替換表達式
Javassist 只允許修改方法體中包含的表達式。javassist.expr.ExprEditor 是一個用於替換方法體中的表達式的類。用戶可以定義 ExprEditor 的子類來指定修改表達式的方式。
要運行 ExprEditor 對象,用戶必須在 CtMethod 或 CtClass 中調用 instrument()。
例如,
-
CtMethod cm = ... ;
-
cm.instrument(
-
new ExprEditor() {
-
public void edit(MethodCall m) throws CannotCompileException {
-
if (m.getClassName(). equals( "Point")
-
&& m.getMethodName(). equals( "move"))
-
m.replace( "{ $1 = 0; $_ = $proceed($$); }");
-
}
-
});
上述代碼,搜索由 cm 表示的方法體,並用使用下面的代碼替換 Point 中的 move()調用:
{ $1 = 0; $_ = $proceed($$); }
因此 move() 的第一個參數總是0。注意,替換的代碼不是一個表達式,而是一個語句或塊。 它不能是或包含 try-catch 語句。
方法 instrument() 搜索方法體。 如果它找到一個表達式,如方法調用、字段訪問和對象創建,那么它調用給定的 ExprEditor 對象上的 edit() 方法。 edit() 的參數表示找到的表達式。 edit() 可以檢查和替換該表達式。
調用 edit() 參數的 replace() 方法可以將表達式替換為我們給定的語句。如果給定的語句是空塊,即執行replace("{}"),則將表達式刪除。如果要在表達式之前或之后插入語句(或塊),則應該將類似以下的代碼傳遞給 replace():
-
{ *before-statements;*
-
$_ = $proceed( $$);
-
*after-statements;* }
無論表達式是方法調用、字段訪問還是對象創建或其他。
如果表達式是讀操作,第二個語句應該是:
$_ = $proceed();
如果表達式是寫操作,則第二個語句應該是:
$proceed($$);
如果由 instrument() 搜索的方法是使用 -g 選項(類文件包含一個局部變量屬性)編譯的,目標表達式中可用的局部變量,也可以傳遞給 replace() 的源代碼中使用。
javassist.expr.MethodCall
MethodCall 表示方法調用。MethodCall 的 replace() 方法用於替換方法調用,它接收表示替換語句或塊的源代碼。和 insertBefore() 方法一樣,傳遞給 replace 的源代碼中,以 $ 開頭的標識符具有特殊的含義。
符號 | 含義 |
---|---|
$0 |
方法調用的目標對象。它不等於 this,它代表了調用者。 如果方法是靜態的,則 $0 為 null |
$1 , $2 .. |
方法的參數 |
$_ |
方法調用的結果 |
$r |
返回結果的類型,用於強制類型轉換 |
$class |
一個 java.lang.Class 對象,表示當前正在修改的類 |
$sig |
類型為 java.lang.Class 的參數類型數組 |
$type |
一個 java.lang.Class 對象,表示返回值類型 |
$class |
一個 java.lang.Class 對象,表示當前正在修改的類 |
$proceed |
調用表達式中方法的名稱 |
這里的方法調用意味着由 MethodCall 對象表示的方法。
其他標識符如 $w,$args 和 $$ 也可用。
除非方法調用的返回類型為 void,否則返回值必須在源代碼中賦給 $_,$_ 的類型是表達式的結果類型。如果結果類型為 void,那么 $_ 的類型為Object,並且分配給 $_ 的值將被忽略。
$proceed 不是字符串值,而是特殊的語法。 它后面必須跟一個由括號括起來的參數列表。
javassist.expr.ConstructorCall
ConstructorCall 表示構造函數調用,例如包含在構造函數中的 this() 和 super()。ConstructorCall 中的方法 replace() 可以使用語句或代碼塊來代替構造函數。它接收表示替換語句或塊的源代碼。和 insertBefore() 方法一樣,傳遞給 replace 的源代碼中,以 $ 開頭的標識符具有特殊的含義。
符號 | 含義 |
---|---|
$0 |
構造調用的目標對象。它等於 this |
$1 , $2 , ... |
構造函數的參數 |
$class |
一個 java.lang.Class 對象,表示當前正在修改的類 |
$sig |
類型為 java.lang.Class 的參數類型數組 |
$proceed |
調用表達式中構造函數的名稱 |
這里的構造函數調用是由 ConstructorCall 對象表示的。
其他標識符如 $w,$args 和 $$ 也可用。
由於任何構造函數必須調用超類的構造函數或同一類的另一個構造函數,所以替換語句必須包含構造函數調用,通常是對 $proceed() 的調用。
$proceed 不是字符串值,而是特殊的語法。 它后面必須跟一個由括號括起來的參數列表。
javassist.expr.FieldAccess
FieldAccess 對象表示字段訪問。 如果找到對應的字段訪問操作,ExprEditor 中的 edit() 方法將接收到一個 FieldAccess 對象。FieldAccess 中的 replace() 方法接收替源代碼來替換字段訪問。
在源代碼中,以 $ 開頭的標識符具有特殊含義:
符號 | 含義 |
---|---|
$0 |
表達式訪問的字段。它不等於 this。this 表示調用表達式所在方法的對象。如果字段是靜態的,則 $0 為 null |
$1 |
如果表達式是寫操作,則寫的值將保存在 $1 中。否則 $1 不可用 |
$_ |
如果表達式是讀操作,則結果值保存在 $1 中,否則將舍棄存儲在 $_ 中的值 |
$r |
如果表達式是讀操作,則 $r 讀取結果的類型。 否則 $r 為 void |
$class |
一個 java.lang.Class 對象,表示字段所在的類 |
$type |
一個 java.lang.Class 對象,表示字段的類型 |
$proceed |
執行原始字段訪問的虛擬方法的名稱 |
其他標識符如 $w,$args 和 $$ 也可用。
如果表達式是讀操作,則必須在源文本中將值分配給 $。 $的類型是字段的類型。
javassist.expr.NewExpr
NewExpr 表示使用 new 運算符(不包括數組創建)創建對象的表達式。 如果發現創建對象的操作,NewEditor 中的 edit() 方法將接收到一個 NewExpr 對象。NewExpr 中的 replace() 方法接收替源代碼來替換字段訪問。
在源文本中,以 $ 開頭的標識符具有特殊含義:
符號 | 含義 |
---|---|
$0 |
null |
$1 |
構造函數的參數 |
$_ |
創建對象的返回值。一個新的對象存儲在 $_ 中 |
$r |
所創建的對象的類型 |
$sig |
類型為 java.lang.Class 的參數類型數組 |
$type |
一個 java.lang.Class 對象,表示創建的對象的類型 |
$proceed |
執行對象創建虛擬方法的名稱 |
其他標識符如 $w,$args 和 $$ 也可用。
javassist.expr.NewArray
NewArray 表示使用 new 運算符創建數組。如果發現數組創建的操作,ExprEditor 中的 edit() 方法一個 NewArray 對象。NewArray 中的 replace() 方法可以使用源代碼來替換數組創建操作。
在源文本中,以$開頭的標識符具有特殊含義:
符號 | 含義 |
---|---|
$0 |
null |
$1 , $1 |
每一維的大小 |
$_ |
創建數組的返回值。一個新的數組對象存儲在 $_ 中 |
$r |
所創建的數組的類型 |
$type |
一個 java.lang.Class 對象,表示創建的數組的類型 |
$proceed |
執行數組創建虛擬方法的名稱 |
其他標識符如 $w,$args 和 $$ 也可用。
例如,如果按下面的方式創建數組:
String[][] s = new String[3][4];
那么 $1 和 $2 的值分別是 3 和 4。 $3 不可用。
例如,如果按下面的方式創建數組:
String[][] s = new String[3][];
那么 $1 的值為 3,但 $2 不可用。
javassist.expr.Instanceof
一個 InstanceOf 對象表示一個 instanceof 表達式。 如果找到 instanceof 表達式,則ExprEditor 中的 edit() 方法接收此對象。Instanceof 中的 replace() 方法可以使用源代碼來替換 instanceof 表達式。
在源文本中,以$開頭的標識符具有特殊含義:
符號 | 含義 |
---|---|
$0 |
null |
$1 |
instanceof 運算符左側的值 |
$_ |
表達式的返回值。類型為 boolean |
$r |
instanceof 運算符右側的值 |
$type |
一個 java.lang.Class 對象,表示 instanceof 運算符右側的類型 |
$proceed |
執行 instanceof 表達式的虛擬方法的名稱。它需要一個參數(類型是 java.lang.Object)。如果參數類型和 instanceof 表達式右側的類型一致,則返回 true。否則返回 false。 |
其他標識符如 $w,$args 和 $$ 也可用。
javassist.expr.Cast
Cast 表示 cast 表達式。如果找到 cast 表達式,ExprEditor 中的 edit() 方法會接收到一個 Cast 對象。 Cast 的 replace() 方法可以接收源代碼來替換替換 �cast 表達式。
在源文本中,以$開頭的標識符具有特殊含義:
符號 | 含義 |
---|---|
$0 |
null |
$1 |
顯示類型轉換的目標類型(?) |
$_ |
表達式的結果值。$_ 的類型和被括號括起來的類型相同(?) |
$r |
轉換之后的類型,即被括號括起來的類型(?) |
$type |
一個 java.lang.Class 對象,和 $r 的類型相同 |
$proceed |
執行類型轉換的虛擬方法的名稱。它需要一個參數(類型是 java.lang.Object)。並在類型轉換完成后返回它 |
其他標識符如 $w,$args 和 $$ 也可用。
javassist.expr.Handler
Handler 對象表示 try-catch 語句的 catch 子句。 如果找到 catch,ExprEditor 中的 edit() 方法會接收此對象。 Handler 中的 insertBefore() 方法會將收到的源代碼插入到 catch 子句的開頭。
在源文本中,以$開頭的標識符具有意義:
符號 | 含義 |
---|---|
$1 |
catch 分支獲得的異常對象 |
$r |
catch 分支獲得的異常對象的類型,用於強制類型轉換 |
$w |
包裝類型,用於強制類型轉換 |
$type |
一個 java.lang.Class 對象,表示 catch 捕獲的異常的類型 |
如果一個新的異常分配給 $1,它將作為捕獲的異常傳遞給原始的 catch 子句。
4.3 添加新方法和字段
添加新方法
Javassist 可以創建新的方法和構造函數。CtNewMethod 和 CtNewConstructor 提供了幾個工廠方法來創建 CtMethod 或 CtConstructor 對象。make() 方法可以通過源代碼來CtMethod 或 CtConstructor 對象。
例如:
-
CtClass point = ClassPool.getDefault().get( "Point");
-
CtMethod m = CtNewMethod.make(
-
"public int xmove(int dx) { x += dx; }",
-
point);
-
point.addMethod(m);
上面的代碼向類 Point 添加了一個公共方法 xmove()。在這個例子中,x 是類 Point 的一個int 字段。
傳遞給 make() 和 setBody() 的源文本可以包括以 $ 開頭的標識符 ($_ 除外)。 如果目標對象和目標方法名也被傳遞給 make() 方法,源文本中也可以包括 $proceed。
例如:
-
CtClass point = ClassPool.getDefault(). get( "Point");
-
CtMethod m = CtNewMethod.make(
-
"public int ymove(int dy) { $proceed(0, dy); }",
-
point, "this", "move");
這個程序創建一個 ymove() 方法,定義如下:
public int ymove(int dy) { this.move(0, dy); }
注意,$proceed 已經被替換為 this.move。
Javassist 還提供了另一種添加新方法的方式。 你可以先創建一個抽象方法,然后給它一個方法體:
-
CtClass cc = ... ;
-
CtMethod m = new CtMethod(CtClass.intType, "move",
-
new CtClass[] { CtClass.intType }, cc);
-
cc.addMethod(m);
-
m.setBody( "{ x += $1; }");
-
cc.setModifiers(cc.getModifiers() & ~Modifier. ABSTRACT);
因為 Javassist 在類中添加了的方法是抽象的,所以在調用 setBody() 之后,必須將類顯式地改回非抽象類。
相互遞歸的方法 (Mutual recursive methods)
Javassist 不能這種方法:如果它調用另一個方法,而另一個方法沒有被添加到一個類(Javassist可以編譯一個以遞歸方式調用的方法)。如果要向類添加相互遞歸方法,需要使用如下的技巧。假設你想要將方法 m() 和 n() 添加到由 cc 表示的類中:
-
CtClass cc = ... ;
-
CtMethod m = CtNewMethod.make( "public abstract int m(int i);", cc);
-
CtMethod n = CtNewMethod.make( "public abstract int n(int i);", cc);
-
cc.addMethod(m);
-
cc.addMethod(n);
-
m.setBody( "{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }");
-
n.setBody( "{ return m($1); }");
-
cc.setModifiers(cc.getModifiers() & ~Modifier. ABSTRACT);
你必須先創建兩個抽象方法,並將它們添加到類中。然后設置它們的方法體,即使方法體包括互相遞歸的調用。 最后,必須將類更改為非抽象類。
添加一個字段
Javassist 還允許用戶創建一個新字段。
-
CtClass point = ClassPool.getDefault().get( "Point");
-
CtField f = new CtField(CtClass.intType, "z", point);
-
point.addField(f);
該程序向類 Point 添加一個名為 z 的字段。
如果必須指定添加字段的初始值,那么上面的程序必須修改為:
-
CtClass point = ClassPool.getDefault().get( "Point");
-
CtField f = new CtField(CtClass.intType, "z", point);
-
point.addField(f, "0"); // initial value is 0
現在,方法 addField() 接收兩個參數,第二個參數表示計算初始值的表達式。這個表達式可以是任意 Java 表達式,只要其結果與字段的類型匹配。 請注意,表達式不以分號結尾。
此外,上述代碼可以重寫為更簡單代碼:
-
CtClass point = ClassPool.getDefault().get( "Point");
-
CtField f = CtField.make( "public int z = 0;", point);
-
point.addField(f);
刪除成員
要刪除字段或方法,請在 CtClass 的 removeField() 或 removeMethod() 方法。 一個CtConstructor 可以通過 CtClass 的 removeConstructor() 刪除。
4.4 注解 (Annotations)
CtClass,CtMethod,CtField 和 CtConstructor 提供 getAnnotations() 方法,用於讀取注解。 它返回一個注解類型的對象。
例如,假設有以下注解:
-
public @interface Author {
-
String name();
-
int year();
-
}
下面是使用注解的代碼:
-
-
public class Point {
-
int x, y;
-
}
然后,可以使用 getAnnotations() 獲取注解的值。 它返回一個包含注解類型對象的數組。
-
CtClass cc = ClassPool.getDefault(). get("Point");
-
Object[] all = cc.getAnnotations();
-
Author a = (Author) all[ 0];
-
String name = a.name();
-
int year = a.year();
-
System.out.println("name: " + name + ", year: " + year);
這段代碼輸出:
name: Chiba, year: 2005
由於 Point 的注解只有 @Author,所以數組的長度是 1,all[0] 是一個 Author 對象。 注解成員值可以通過調用Author對象的 name() 和 year() 來獲取。
要使用 getAnnotations(),注釋類型(如 Author)必須包含在當前類路徑中。它們也必須也可以從 ClassPool 對象訪問。如果未找到注釋類型的類文件,Javassist 將無法獲取該注釋類型的成員的默認值。
4.5 運行時支持類
在大多數情況下,使用 Javassist 修改類不需要運行 Javassist。 但是,Javassist 編譯器生成的某些字節碼需要運行時支持類,這些類位於 javassist.runtime 包中(有關詳細信息,請閱讀該包的API文檔)。請注意,javassist.runtime 是修改的類時唯一可能需要使用的包。 修改類的運行時不會再使用其他的 Javassist 類。
4.6 導入(Import)
源代碼中的所有類名都必須是完整的(必須包含包名,java.lang 除外)。例如,Javassist 編譯器可以解析 Object 以及 java.lang.Object。
要告訴編譯器在解析類名時搜索其他包,請在 ClassPool中 調用 importPackage()。 例如,
-
ClassPool pool = ClassPool.getDefault();
-
pool.importPackage( "java.awt");
-
CtClass cc = pool.makeClass( "Test");
-
CtField f = CtField.make( "public Point p;", cc);
-
cc.addField(f);
第二行導入了 java.awt 包。 因此,第三行不會拋出異常。 編譯器可以將 Point 識別為java.awt.Point。
注意 importPackage() 不會影響 ClassPool 中的 get() 方法。只有編譯器才考慮導入包。 get() 的參數必須是完整類名。
4.7 限制 (Limitations)
在目前實現中,Javassist 中包含的 Java 編譯器有一些限制:
- J2SE 5.0 引入的新語法(包括枚舉和泛型)不受支持。注釋由 Javassist 的低級 API 支持。 參見 javassist.bytecode.annotation 包(以及 CtClass 和 CtBehavior 中的� getAnnotations())。對泛型只提供部分支持。更多信息,請參閱后面的部分;
- 初始化數組時,只有一維數組可以用大括號加逗號分隔元素的形式初始化,多維數組還不支持;
- 編譯器不能編譯包含內部類和匿名類的源代碼。 但是,Javassist 可以讀取和修改內部/匿名類的類文件;
- 不支持帶標記的 continue 和 break 語句;
- 編譯器沒有正確實現 Java 方法調度算法。編譯器可能會混淆在類中定義的重載方法(方法名稱相同,查參數列表不同)。例如:
-
class A {}
-
class B extends A {}
-
class C extends B {}
-
class X {
-
void foo( A a) { .. }
-
void foo( B b) { .. }
-
}
如果編譯的表達式是 x.foo(new C())
,其中 x
是 X
的實例,編譯器將產生對 foo(A)
的調用,盡管編譯器可以正確地編譯 foo((B) new C())
。
- 建議使用 # 作為類名和靜態方法或字段名之間的分隔符。 例如,在常規 Java 中,
javassist.CtClass.intType.getName()
在 javassist.CtClass 中的靜態字段 intType 指示的對象上調用一個方法 getName()。 在Javassist 中,用戶也可以寫上面的表達式,但是建議寫成這樣:
javassist.CtClass#intType.getName()
使編譯器可以快速解析表達式。
5. 字節碼操作
Javassist 還提供了用於直接編輯類文件的低級級 API。 使用此 API之前,你需要詳細了解Java 字節碼和類文件格式,因為它允許你對類文件進行任意修改。
如果你只想生成一個簡單的類文件,使用javassist.bytecode.ClassFileWriter
就足夠了。 它比javassist.bytecode.ClassFile
更快而且更小。
獲取 ClassFile 對象
javassist.bytecode.ClassFile 對象表示類文件。要獲得這個對象,應該調用 CtClass 中的 getClassFile() 方法。
你也可以直接從類文件構造 javassist.bytecode.ClassFile 對象。 例如:
-
BufferedInputStream fin
-
= new BufferedInputStream( new FileInputStream( "Point.class"));
-
ClassFile cf = new ClassFile( new DataInputStream(fin));
這代碼段從 Point.class 創建一個 ClassFile 對象。
ClassFile 對象可以寫回類文件。ClassFile 的 write() 將類文件的內容寫入給定的 DataOutputStream。
5.2 添加和刪除成員
ClassFile 提供了 addField(),addMethod() 和 addAttribute(),來向類添加字段、方法和類文件屬性。
注意,FieldInfo,MethodInfo 和 AttributeInfo 對象包括到 ConstPool(常量池表)對象的鏈接。 ConstPool 對象必須對 ClassFile 對象和添加到該 ClassFile 對象的 FieldInfo(或MethodInfo 等)對象是通用的。 換句話說,FieldInfo(或MethodInfo等)對象不能在不同的ClassFile 對象之間共享。
要從 ClassFile 對象中刪除字段或方法,必須首先獲取包含該類的所有字段的 java.util.List 對象。 getFields() 和 getMethods() 返回列表。可以通過在List對象上調用 remove() 來刪除字段或方法。可以以類似的方式去除屬性。在 FieldInfo 或 MethodInfo 中調用 getAttributes() 以獲取屬性列表,並從列表中刪除一個。
5.3 遍歷方法體
使用 CodeIterator 可以檢查方法體中的每個字節碼指令,要獲得 CodeIterator 對象,參考以下代碼:
-
ClassFile cf = ... ;
-
MethodInfo minfo = cf.getMethod( "move"); // we assume move is not overloaded.
-
CodeAttribute ca = minfo.getCodeAttribute();
-
CodeIterator ci = ca.iterator();
CodeIterator 對象允許你逐個訪問每個字節碼指令。下面展示了一部分 CodeIterator 中聲明的方法:
- void begin()
移動到第一條指令。 - void move(int index)
移動到指定位置的指令。 - boolean hasNext()
是否有下一條指定 - int next()
返回下一條指令的索引。注意,它不返回下一條指令的操作碼。 - int byteAt(int index)
返回索引處的無符號8位整數。 - int u16bitAt(int index)
返回索引處的無符號16位整數。 - int write(byte [] code,int index)
在索引處寫入字節數組。 - void insert(int index,byte [] code)
在索引處插入字節數組。自動調整分支偏移量。
以下代碼段打印了方法體中所有的指令:
-
CodeIterator ci = ... ;
-
while (ci.hasNext()) {
-
int index = ci.next();
-
int op = ci.byteAt( index);
-
System.out.println(Mnemonic.OPCODE[op]);
-
}
5.4 生成字節碼序列
Bytecode
對象表示字節碼指令序列。它是一個可擴展的字節碼數組。
以下是示例代碼段:
-
ConstPool cp = ...; // constant pool table
-
Bytecode b = new Bytecode(cp, 1, 0);
-
b.addIconst( 3);
-
b.addReturn(CtClass.intType);
-
CodeAttribute ca = b.toCodeAttribute();
這段代碼產生以下序列的代碼屬性:
-
iconst_3
-
ireturn
您還可以通過調用 Bytecode 中的 get() 方法來獲取包含此序列的字節數組。獲得的數組可以插入另一個代碼屬性。
Bytecode 提供了許多方法來添加特定的指令,例如使用 addOpcode() 添加一個 8 位操作碼,使用 addIndex() 用於添加一個索引。每個操作碼的值定義在 Opcode 接口中。
addOpcode() 和添加特定指令的方法,將自動維持最大堆棧深度,除非控制流沒有分支。可以通過調用 Bytecode 的 getMaxStack() 方法來獲得這個深度。它也反映在從 Bytecode對象構造的 CodeAttribute 對象上。要重新計算方法體的最大堆棧深度,可以調用 CodeAttribute 的 computeMaxStack() 方法。
5.5 注釋(元標簽)
注釋作為運行時不可見(或可見)的注記屬性,存儲在類文件中。調用 getAttribute(AnnotationsAttribute.invisibleTag)方法,可以從 ClassFile,MethodInfo 或 FieldInfo 中獲取注記屬性。更多信息,請參閱 javassist.bytecode.AnnotationsAttribute
和javassist.bytecode.annotation
包的 javadoc 手冊。
Javassist還允許您通過更高級別的API訪問注釋。 如果要通過CtClass訪問注釋,請在CtClass或CtBehavior中調用getAnnotations()。
6. 泛型
Javassist 的低級別 API 完全支持 Java 5 引入的泛型。但是,高級別的API(如CtClass)不直接支持泛型。
Java 的泛型是通過擦除技術實現。 編譯后,所有類型參數都將被刪除。 例如,假設您的源代碼聲明一個參數化類型 Vector<String>:
-
Vector< String> v = new Vector< String>();
-
:
-
String s = v. get( 0);
編譯后的字節碼等價於以下代碼:
-
Vector v = new Vector();
-
:
-
String s = ( String)v. get( 0);
因此,在編寫字節碼變換器時,您可以刪除所有類型參數,因為 Javassist 的編譯器不支持泛型。如果源代碼使用 Javassist 編譯,例如通過 CtMethod.make(),源代碼必須顯式類型轉換。如果源代碼由常規 Java 編譯器(如javac)編譯,則不需要做類型轉換。
例如,如果你有一個類:
-
public class Wrapper< T> {
-
T value;
-
public Wrapper(T t) { value = t; }
-
}
並想添加一個接口 Getter<T> 到類 Wrapper<T>:
-
public interface Getter< T> {
-
T get();
-
}
那么你真正要添加的接口其實是Getter(將類型參數<T>掉落),最后你添加到 Wrapper 類的方法是這樣的:
public Object get() { return value; }
注意,不需要類型參數。 由於 get 返回一個 Object,如果源代碼是由 Javassist 編譯的,那么在調用方需要進行顯式類型轉換。 例如,如果類型參數 T 是 String,則必須插入(String),如下所示:
-
Wrapper w = ...
-
String s = ( String)w. get();
7.可變參數
目前,Javassist 不直接支持可變參數。 因此,要使用 varargs 創建方法,必須顯式設置方法修飾符。假設要定義下面這個方法:
public int length(int... args) { return args.length; }
使用 Javassist 應該是這樣的:
-
CtClass cc = /* target class */;
-
CtMethod m = CtMethod.make( "public int length(int[] args) { return args.length; }", cc);
-
m.setModifiers(m.getModifiers() | Modifier.VARARGS);
-
cc.addMethod(m);
參數類型int ...
被更改為int []
,Modifier.VARARGS
被添加到方法修飾符中。
要在由 Javassist 的編譯器編譯的源代碼中調用此方法,需要這樣寫:
length(new int[] { 1, 2, 3 });
而不是這樣:
length(1, 2, 3);
8. J2ME
如果要修改 J2ME 執行環境的類文件,則必須先執行預驗證。預驗證基本上是生成堆棧映射,這類似於在 JDK 1.6 中引入 J2SE 的堆棧映射表。當javassist.bytecode.MethodInfo.doPreverify
為 true 時,Javassist 才會維護 J2ME 的堆棧映射。
對於指定的 CtMethod 對象,你可以調用以下方法,手動生成堆棧映射:
m.getMethodInfo().rebuildStackMapForME(cpool);
這里,cpool 是一個 ClassPool 對象,通過在 CtClass 對象上調用 getClassPool() 可以獲得。 ClassPool 對象負責從給定類路徑中查找類文件。要獲得所有的 CtMethod 對象,需要在 CtClass 對象上調用 getDeclaredMethods() 方法。
9.裝箱/拆箱
Java 中的裝箱和拆箱是語法糖。沒有用於裝箱或拆箱的字節碼。所以 Javassist 的編譯器不支持它們。 例如,以下語句在 Java 中有效:
Integer i = 3;
因為隱式地執行了裝箱。 但是,對於 Javassist,必須將值類型從 int 顯式地轉換為 Integer:
Integer i = new Integer(3);
10. 調試
將 CtClass.debugDump 設為本地目錄。 然后 Javassist 修改和生成的所有類文件都保存在該目錄中。要停止此操作,將 CtClass.debugDump 設置為 null 即可。其默認值為 null。
例如,
CtClass.debugDump =“./dump”;
所有修改的類文件都保存在 ./dump 中。
-------------------------------------------------------
原文出處:
https://github.com/jboss-javassist/javassist/wiki/Tutorial-1
https://github.com/jboss-javassist/javassist/wiki/Tutorial-2
https://github.com/jboss-javassist/javassist/wiki/Tutorial-3
翻譯作者:二胡
鏈接:https://www.jianshu.com/p/7803ffcc81c8