Java 编程的动态性,第 7 部分: 用 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.InstructionFactoryhelper 類。使用?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 toolsInstructionFactory 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 methodMethodGen 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 parametersType[] 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 invocationilist.append(ifact.createInvoke("java.lang.System","currentTimeMillis", Type.LONG, Type.NO_ARGS, Constants.INVOKESTATIC));ilist.append(InstructionFactory.createStore(Type.LONG, slot));// call the wrapped methodint 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 laterif (result != Type.VOID) {ilist.append(InstructionFactory.createStore(result, slot+2));}// print time required for method callilist.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 callif (result != Type.VOID) {ilist.append(InstructionFactory.createLoad(result, slot+2));}ilist.append(InstructionFactory.createReturn(result));// finalize the constructed methodwrapgen.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 編程的動態性以了解詳情。
原文:http://www.ibm.com/developerworks/cn/java/j-dyn0414/index.html
轉載于:https://www.cnblogs.com/davidwang456/p/4035858.html
總結
以上是生活随笔為你收集整理的Java 编程的动态性,第 7 部分: 用 BCEL 设计字节码--转载的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 编程的动态性,第 6 部分:
- 下一篇: Java 编程的动态性,第 8 部分: