Apache Byte Code Engineering Library (BCEL)可以深入 Java 類的字節碼。可以用它 轉換現有的類表示或者構建新的類,因為 BCEL 在單獨的 JVM 指令級別上進行操作,所以可 以讓您對代碼有最強大的控制。不過,這種能力的代價是復雜性。在本文中,Java 顧問 Dennis Sosnoski 介紹了 BCEL 的基本內容,並引導讀者完成一個示例 BCEL 應用程序,這 樣您就可以自己決定是否值得以這種復雜性來換取這種能力。
在本系列的最後三篇文章中,我展示了如何用 Javassist 框架操作類。這次我將用一種很 不同的方法操縱字節碼——使用 Apache Byte Code Engineering Library (BCEL)。與 Javassist 所支持的源代碼接口不同,BCEL 在實際的 JVM 指令層次上進行操作。在希望對 程序執行的每一步進行控制時,底層方法使 BCEL 很有用,但是當兩者都可以勝任時,它也 使 BCEL 的使用比 Javassist 要復雜得多。
我將首先討論 BCEL 基本體系結構,然後本文的大部分內容將討論用 BCEL 重新構建我的 第一個 Javassist 類操作的例子。最後簡要介紹 BCEL 包中提供的一些工具和開發人員用 BCEL 構建的一些應用程序。
BCEL 類訪問
BCEL 使您能夠同樣具備 Javassist 提供的分析、編輯和創建 Java 二進制類的所有基本 能力。BCEL 的一個明顯區別是每項內容都設計為在 JVM 匯編語言的級別、而不是 Javassist 所提供的源代碼接口上工作。除了表面上的差別,還有一些更深層的區別,包括 在 BCEL 中組件的兩個不同層次結構的使用——一個用於檢查現有的代碼,另一個用於創建 新代碼。我假定讀者已經通過本系列前面的文章熟悉了 Javassist(請參閱側欄 不要錯過本 系列的其余部分)。因此我將主要介紹在開始使用 BCEL 時,可能會讓您感到迷惑的那些不 同之處。
與 Javassist 一樣, BCEL 在類分析方面的功能基本上與 Java 平台通過 Relfection API 直接提供的功能是重復的。這種重復對於類操作工具箱來說是必要的,因為一般不希望 在所要操作的類被修改 之前就裝載它們。
BCEL 在 org.apache.bcel 包中提供了一些基本常量定義,但是除了這些定義,所有分析 相關的代碼都在 org.apache.bcel.classfile 包中。這個包中的起點是 JavaClass 類。這 個類在用 BCEL 訪問類信息時起的作用與使用常規 Java 反射時, java.lang.Class 的作用 一樣。 JavaClass 定義了得到這個類的字段和方法信息,以及關於父類和接口的結構信息的 方法。 與 java.lang.Class 不同,JavaClass 還提供了對類的內部信息的訪問,包括常量 池和屬性,以及作為字節流的完整二進制類表示。
JavaClass 實例通常是通過解析實際的二進制類創建的。BCEL 提供了 org.apache.bcel.Repository 類用於處理解析。在默認情況下,BCEL 解析並緩沖在 JVM 類 路徑中找到的類表示,從 org.apache.bcel.util.Repository 實例中得到實際的二進制類表 示(注意包名的不同)。 org.apache.bcel.util.Repository 實際上是二進制類表示的源代 碼的接口。在默認源代碼中使用類路徑的地方,可以用查詢類文件的其他路徑或者其他訪問 類信息的方法替換。
改變類
除了對類組件的反射形式的訪問, org.apache.bcel.classfile.JavaClass 還提供了改 變類的方法。可以用這些方法將任何組件設置為新值。不過一般不直接使用它們,因為包中 的其他類不以任何合理的方式支持構建新版本的組件。相反,在 org.apache.bcel.generic 包中有完全單獨的一組類,它提供了 org.apache.bcel.classfile 類所表示的同一組件的可 編輯版本。
就 像 org.apache.bcel.classfile.JavaClass 是使用 BCEL 分析現有類的起點一樣, org.apache.bcel.generic.ClassGen 是創建新類的起點。它還用於修改現有的類——為了處 理這種情況,有一個以 JavaClass 實例為參數的構造函數,並用它初始化 ClassGen 類信息 。修改了類以後,可以通過調用一個返回 JavaClass 的方法從 ClassGen 實例得到可使用的 類表示,它又可以轉換為一個二進制類表示。
聽起來有些亂?我想是的。事實上,在兩個包之間來回轉是使用 BCEL 的一個最主要的缺 點。重復的類結構總有些礙手礙腳,所以如果頻繁使用 BCEL,那麼可能需要編寫一個包裝器 類,它可以隱藏其中一些不同之處。在本文中,我將主要使用 org.apache.bcel.generic 包 類,並避免使用包裝器。不過在您自己進行開發時要記住這一點。
除了 ClassGen , org.apache.bcel.generic 包還定義了管理不同類組件的結構的類。 這些結構類包括用於處理常量池的 ConstantPoolGen 、用於字段和方法的 FieldGen 和 MethodGen 和處理一系列 JVM 指令的 InstructionList 。最後, org.apache.bcel.generic 包還定義了表示每一種類型的 JVM 指令的類。可以直接創建這些 類的實例,或者在某些情況下使用 org.apache.bcel.generic.InstructionFactory helper 類。使用 InstructionFactory 的好處是它處理了許多指令構建的簿記細節(包括根據指令 的需要在常量池中添加項)。在下面一節您將會看到如何使所有這些類協同工作。
用 BCEL 進行類操作
作為使用 BCEl 的一個例子,我將使用 第 4 部分中的一個 Javassist 例子——測量執 行一個方法的時間。我甚至采用了與使用 Javassist 時的相同方式:用一個改過的名字創建 要計時的原方法的一個副本,然後,通過調用改名後的方法,利用包裝了時間計算的代碼來 替換原方法的主體。
選擇一個試驗品
清單 1 給出了一個用於展示目的示例方法: StringBuilder 類的 buildString 方法。 正如我在 第 4 部分所說的,這個方法采用了所有 Java 性能專家告誡您 不要 使用的方式 來構建一個 String —— 它重復地在字符串的未尾附加單個字符以創建更長的字符串。因為 字符串是不可變的,所以這種方式意味著每次循環時會構建一個新的字符串,從老的字符串 拷貝數據並在最後增加一個字符。總的效果就是用這個方法創建更長的字符串時,它會產生 越來越大的開銷。
清單 1. 要計時的方法
public class StringBuilder
{
private String buildString(int length) {
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
return result;
}
public static void main(String[] argv) {
StringBuilder inst = new StringBuilder();
for (int i = 0; i < argv.length; i++) {
String result = inst.buildString(Integer.parseInt(argv [i]));
System.out.println("Constructed string of length " +
result.length());
}
}
}
清單 2 顯示了等同於用 BCEL 進行類操作改變的源代碼。這裡包裝器方法只是保存當前 時間,然後調用改名後的原方法,並在返回調用原方法的結果之前打印時間報告。
清單 2. 在原方法中加入計時
public class StringBuilder
{
private String buildString$impl(int length) {
String result = "";
for (int i = 0; i < length; i++) {
result += (char)(i%26 + 'a');
}
return result;
}
private String buildString(int length) {
long start = System.currentTimeMillis();
String result = buildString$impl(length);
System.out.println("Call to buildString$impl took " +
(System.currentTimeMillis()-start) + " ms.");
return result;
}
public static void main(String[] argv) {
StringBuilder inst = new StringBuilder();
for (int i = 0; i < argv.length; i++) {
String result = inst.buildString(Integer.parseInt(argv [i]));
System.out.println("Constructed string of length " +
result.length());
}
}
}
編寫轉換代碼
用我在 BCEL 類訪問一節中描述的 BCEL API 實現添加方法計時的代碼。在 JVM 指令級 別上的操作使代碼比 第 4 部分 中 Javassist 的例子要長得多,所以這裡我准備在提供完 整的實現之前,一段一段地介紹。在最後的代碼中,所有片段構成一個方法,它有兩個參數 : cgen ——它是 org.apache.bcel.generic.ClassGen 類的一個實例,用被修改的類的現 有信息初始化,和方法——要計時方法的 org.apache.bcel.classfile.Method 實例。
清單 3 是轉換方法的第一段代碼。可以從注釋中看到,第一部分只是初始化要使用的基 本 BCEL 組件,它包括用要計時方法的信息初始化一個新的 org.apache.bcel.generic.MethodGen 實例。我為這個 MethodGen 設置一個空的指令清單, 在後面我將用實際的計時代碼填充它。在第 2 部分,我用原來的方法創建第二個 org.apache.bcel.generic.MethodGen 實例,然後從類中刪除原來的方法。在第二個 MethodGen 實例中,我只是讓名字加上“$impl”後綴,然後調用 getMethod() 以將可修改 的方法信息轉換為固定形式的 org.apache.bcel.classfile.Method 實例。然後調用 addMethod() 以便在類中添加改名後的方法。
清單 3. 添加攔截方法
// set up the construction tools
InstructionFactory ifact = new InstructionFactory(cgen);
InstructionList ilist = new InstructionList();
ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
MethodGen wrapgen = new MethodGen(method, cname, pgen);
wrapgen.setInstructionList(ilist);
// rename a copy of the original method
MethodGen methgen = new MethodGen(method, cname, pgen);
cgen.removeMethod(method);
String iname = methgen.getName() + "$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());
清單 4 給出了轉換方法的下一段代碼。這裡的第一部分計算方法調用參數在堆棧上占用 的空間。之所以需要這段代碼,是因為為了在調用包裝方法之前在堆棧幀上存儲開始時間, 我需要知道局部變量可以使用什麼偏移值(注意,我可以用 BCEL 的局部變量處理得到同樣 的效果,但是在本文中我選擇使用顯式的方式)。這段代碼的第二部分生成對 java.lang.System.currentTimeMillis() 的調用,以得到開始時間,並將它保存到堆棧幀中 計算出的局部變量偏移處。
您可能會奇怪為什麼在開始參數大小計算時要檢查方法是否是靜態的,如果是靜態的,將 堆棧幀槽初始化為零(不是靜態正好相反)。這種方式與 Java 如何處理方法調用有關。對 於非靜態的方法,每次調用的第一個(隱藏的)參數是目標對象的 this 引用,在計算堆棧 幀中完整參數集大小時我要考慮到這點。
清單 4. 設置包裝的調用
// compute the size of the calling parameters
Type[] types = methgen.getArgumentTypes();
int slot = methgen.isStatic() ? 0 : 1;
for (int i = 0; i < types.length; i++) {
slot += types[i].getSize();
}
// save time prior to invocation
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.createStore(Type.LONG, slot));
清單 5 顯示了生成對包裝方法的調用並保存結果(如果有的話)的代碼。這段代碼的第 一部分再次檢查方法是否是靜態的。如果方法不是靜態的,那麼就生成將 this 對象引用裝 載到堆棧中的代碼,同時設置方法調用類型為 virtual (而不是 static )。然後 for 循 環生成將所有調用參數值拷貝到堆棧中的代碼, createInvoke() 方法生成對包裝的方法的 實際調用,最後 if 語句將結果值保存到位於堆棧幀中的另一個局部變量中(如果結果類型 不是 void )。
清單 5. 調用包裝的方法
// call the wrapped method
int offset = 0;
short invoke = Constants.INVOKESTATIC;
if (!methgen.isStatic()) {
ilist.append(InstructionFactory.createLoad(Type.OBJECT, 0));
offset = 1;
invoke = Constants.INVOKEVIRTUAL;
}
for (int i = 0; i < types.length; i++) {
Type type = types[i];
ilist.append(InstructionFactory.createLoad(type, offset));
offset += type.getSize();
}
Type result = methgen.getReturnType();
ilist.append(ifact.createInvoke(cname,
iname, result, types, invoke));
// store result for return later
if (result != Type.VOID) {
ilist.append(InstructionFactory.createStore(result, slot+2));
}
現在開始包裝。清單 6 生成實際計算開始時間後經過的毫秒數,並作為編排好格式的消 息打印出來的代碼。這一部分看上去很復雜,但是大多數操作實際上只是寫出輸出消息的各 個部分。它確實展示了幾種我在前面的代碼中沒有使用的操作類型,包括字段訪問(到 java.lang.System.out )和幾種不同的指令類型。如果將 JVM 想象為基於堆棧的處理器, 則其中大多數是容易理解的,因此我在這裡就不再詳細說明了。
清單 6. 計算並打印所使用的時間
// print time required for method call
ilist.append(ifact.createFieldAccess("java.lang.System", "out",
new ObjectType("java.io.PrintStream"), Constants.GETSTATIC));
ilist.append(InstructionConstants.DUP);
ilist.append(InstructionConstants.DUP);
String text = "Call to method " + methgen.getName() + " took ";
ilist.append(new PUSH(pgen, text));
ilist.append(ifact.createInvoke("java.io.PrintStream", "print",
Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.createLoad(Type.LONG, slot));
ilist.append(InstructionConstants.LSUB);
ilist.append(ifact.createInvoke("java.io.PrintStream", "print",
Type.VOID, new Type[] { Type.LONG }, Constants.INVOKEVIRTUAL));
ilist.append(new PUSH(pgen, " ms."));
ilist.append(ifact.createInvoke("java.io.PrintStream", "println",
Type.VOID, new Type[] { Type.STRING }, Constants.INVOKEVIRTUAL));
生成了計時消息代碼後,留給清單 7 的就是保存包裝的方法的調用結果值(如果有的話 ),然後結束構建的包裝器方法。最後這部分涉及幾個步驟。調用 stripAttributes(true) 只是告訴 BCEL 不對構建的方法生成調試信息,而 setMaxStack() 和 setMaxLocals() 調用 計算並設置方法的堆棧使用信息。完成了這一步後,就可以實際生成方法的最終版本,並將 它加入到類中。
清單 7. 完成包裝器
// return result from wrapped method call
if (result != Type.VOID) {
ilist.append(InstructionFactory.createLoad(result, slot+2));
}
ilist.append(InstructionFactory.createReturn(result));
// finalize the constructed method
wrapgen.stripAttributes(true);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();
完整的代碼
清單 8 顯示了完整的代碼(稍微改變了一下格式以適合顯示寬度),包括以類文件的名 字為參數的 main() 方法和要轉換的方法:
清單 8. 完整的轉換代碼
public class BCELTiming
{
private static void addWrapper(ClassGen cgen, Method method) {
// set up the construction tools
InstructionFactory ifact = new InstructionFactory(cgen);
InstructionList ilist = new InstructionList();
ConstantPoolGen pgen = cgen.getConstantPool();
String cname = cgen.getClassName();
MethodGen wrapgen = new MethodGen(method, cname, pgen);
wrapgen.setInstructionList(ilist);
// rename a copy of the original method
MethodGen methgen = new MethodGen(method, cname, pgen);
cgen.removeMethod(method);
String iname = methgen.getName() + "$impl";
methgen.setName(iname);
cgen.addMethod(methgen.getMethod());
Type result = methgen.getReturnType();
// compute the size of the calling parameters
Type[] types = methgen.getArgumentTypes();
int slot = methgen.isStatic() ? 0 : 1;
for (int i = 0; i < types.length; i++) {
slot += types[i].getSize();
}
// save time prior to invocation
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.
createStore(Type.LONG, slot));
// call the wrapped method
int offset = 0;
short invoke = Constants.INVOKESTATIC;
if (!methgen.isStatic()) {
ilist.append(InstructionFactory.
createLoad(Type.OBJECT, 0));
offset = 1;
invoke = Constants.INVOKEVIRTUAL;
}
for (int i = 0; i < types.length; i++) {
Type type = types[i];
ilist.append(InstructionFactory.
createLoad(type, offset));
offset += type.getSize();
}
ilist.append(ifact.createInvoke(cname,
iname, result, types, invoke));
// store result for return later
if (result != Type.VOID) {
ilist.append(InstructionFactory.
createStore(result, slot+2));
}
// print time required for method call
ilist.append(ifact.createFieldAccess("java.lang.System",
"out", new ObjectType("java.io.PrintStream"),
Constants.GETSTATIC));
ilist.append(InstructionConstants.DUP);
ilist.append(InstructionConstants.DUP);
String text = "Call to method " + methgen.getName() +
" took ";
ilist.append(new PUSH(pgen, text));
ilist.append(ifact.createInvoke("java.io.PrintStream",
"print", Type.VOID, new Type[] { Type.STRING },
Constants.INVOKEVIRTUAL));
ilist.append(ifact.createInvoke("java.lang.System",
"currentTimeMillis", Type.LONG, Type.NO_ARGS,
Constants.INVOKESTATIC));
ilist.append(InstructionFactory.
createLoad(Type.LONG, slot));
ilist.append(InstructionConstants.LSUB);
ilist.append(ifact.createInvoke("java.io.PrintStream",
"print", Type.VOID, new Type[] { Type.LONG },
Constants.INVOKEVIRTUAL));
ilist.append(new PUSH(pgen, " ms."));
ilist.append(ifact.createInvoke("java.io.PrintStream",
"println", Type.VOID, new Type[] { Type.STRING },
Constants.INVOKEVIRTUAL));
// return result from wrapped method call
if (result != Type.VOID) {
ilist.append(InstructionFactory.
createLoad(result, slot+2));
}
ilist.append(InstructionFactory.createReturn(result));
// finalize the constructed method
wrapgen.stripAttributes(true);
wrapgen.setMaxStack();
wrapgen.setMaxLocals();
cgen.addMethod(wrapgen.getMethod());
ilist.dispose();
}
public static void main(String[] argv) {
if (argv.length == 2 && argv[0].endsWith(".class")) {
try {
JavaClass jclas = new ClassParser(argv[0]).parse();
ClassGen cgen = new ClassGen(jclas);
Method[] methods = jclas.getMethods();
int index;
for (index = 0; index < methods.length; index++) {
if (methods[index].getName().equals(argv[1])) {
break;
}
}
if (index < methods.length) {
addWrapper(cgen, methods[index]);
FileOutputStream fos =
new FileOutputStream(argv[0]);
cgen.getJavaClass().dump(fos);
fos.close();
} else {
System.err.println("Method " + argv[1] +
" not found in " + argv[0]);
}
} catch (IOException ex) {
ex.printStackTrace(System.err);
}
} else {
System.out.println
("Usage: BCELTiming class-file method-name");
}
}
}
試一試
清單 9 顯示了以未修改形式第一次運行 StringBuilder 程序的結果,然後運行 BCELTiming 程序以加入計時信息,最後運行修改後的 StringBuilder 程序。可以看到 StringBuilder 在修改後是如何開始報告執行時間的,以及時間為何比構建的字符串長度增 加更快,這是由於字符串構建代碼的效率不高所致。
清單 9. 運行這個程序
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Constructed string of length 1000
Constructed string of length 2000
Constructed string of length 4000
Constructed string of length 8000
Constructed string of length 16000
[dennis]$ java -cp bcel.jar:. BCELTiming StringBuilder.class buildString
[dennis]$ java StringBuilder 1000 2000 4000 8000 16000
Call to method buildString$impl took 20 ms.
Constructed string of length 1000
Call to method buildString$impl took 79 ms.
Constructed string of length 2000
Call to method buildString$impl took 250 ms.
Constructed string of length 4000
Call to method buildString$impl took 879 ms.
Constructed string of length 8000
Call to method buildString$impl took 3875 ms.
Constructed string of length 16000
包裝 BCEL
BCEL 有比我在本文中所介紹的基本類操作支持更多的功能。它還包括完整的驗證器實現 以保證二進制類對於 JVM 規范是有效的(參見 org.apache.bcel.verifier.VerifierFactory ),一個生成很好地分幀並鏈接的 JVM 級二 進制類視圖的反匯編程序,甚至一個 BCEL 程序生成器,它輸出源代碼以讓 BCEL 程序編譯 所提供的類。( org.apache.bcel.util.BCELifier 類沒有包括在 Javadocs 中,所以其用 法要看源代碼。這個功能很吸引人,但是輸出對大多數開發人員來說可能人過於隱晦了)。
我自己使用 BCEL 時,發現 HTML 反匯編程序特別有用。要想試用它,只要執行 BCEL JAR 中的 org.apache.bcel.util.Class2HTML 類,用要反匯編的類文件的路徑作為命令行參 數。它會在當前目錄中生成 HTML 文件。例如,下面我將反匯編在計時例子中使用的 StringBuilder 類:
[dennis]$ java -cp bcel.jar org.apache.bcel.util.Class2HTML StringBuilder.class
Processing StringBuilder.class...Done.
圖 1 是反匯編程序生成的分幀輸出的屏幕快照。在這個快照中,右上角的大幀顯示了添 加到 StringBuilder 類中的計時包裝器方法的分解。在下載文件中有完整的 HTML 輸出—— 如果要實際觀看它,只需在浏覽器窗口中打開 StringBuilder.html 文件。
圖 1. 反匯編 StringBuilder
當前,BCEL 可能是 Java 類操作使用最多的框架。在 Web 網站上列出了一些使用 BCEL 的其他項目,包括 Xalan XSLT 編譯器、Java 編程語言的 AspectJ 擴展和幾個 JDO 實現。 許多其他未列出的項目也使用 BCEL,包括我自己的 JiBX XML 數據綁定項目。不過,BCEL 列出的幾個項目已經轉而使用其他庫,所以不要將這個列表作為 BCEL 大眾化程度的絕對依 據。
BCEL 最大的好處是它的商業友好的 Apache 許可證及其豐富的 JVM 指令級支持。這些功 能結合其穩定性和長壽性,使它成為類操作應用程序的非常流行的選擇。不過,BCEL 看來沒 有設計為具有很好的速度或者容易使用。在大多數情況下,Javassist 提供了更友好的 API ,並有相近的速度(甚至更快),至少在我的簡單測試中是這樣。如果您的項目可以使用 Mozilla Public License (MPL) 或者 GNU Lesser General Public License (LGPL),那麼 Javassist 可能是更好的選擇(它在這兩種許可證下都可以用)。
下一篇
我已經介紹了 Javassist 和 BCEL,本系列的下一篇文章將深入比我們目前已經介紹的用 途更大的類操作應用程序。在 第 2 部分,我展示了方法調用反射比直接調用慢得多。在第 8 部分中,我將顯示如何使用 Javassist 和 BCEL,以便用運行時動態生成的代碼替換反射 調用,從而極大地提高性能。下個月請回來看另一篇 Java 編程的動態性以了解詳情。