Java 程式設計的動態性,第 8 部分: 用程式碼生成取代反射
既然您已經看到了如何使用 Javassist 和 BCEL 框架來進行 classworking (請參閱 本系列以前的一組文章), 我將展示一個實際的 classworking 應用程式。這個應用程式用執行時生成的、並立即裝載到 JVM 的類來取代反射。在綜合討論的過程中,我將引用本系列的前兩篇文章,以及對 Javassist 和 BCEL 的討論,這樣本文就成為了對這個很長的系列文章的一個很好的總結。
反射的效能
在 第 2 部分, 我展示了無論是對於欄位訪問還是方法呼叫,反射都比直接程式碼慢很多倍。這種延緩對於許多應用程式來說不算是問題,但是總是會遇到效能非常關鍵的情況。在這種情況下,反射可能成為真正的瓶頸。但是,用靜態編譯的程式碼取代反射可能會非常混亂,並且在有些情況下(如在這種框架中:反射訪問的類或者專案是在執行時提供的,而不是作為這一編譯過程的一部分提供的),如果不重新構建整個應用程式就根本不可能取代。
Classworking 使我們有機會將靜態編譯的程式碼的效能與反射的靈活性結合起來。這裡的基本方法是,在執行時,以一種可以被一般性程式碼使用的方式,構建一個自定義的類,其中將包裝對目標類的訪問(以前是通過反射達到的)。將這個自定義類裝載到 JVM 中後,就可以全速運行了。
設定階段
清單 1 給出了應用程式的起點。這裡定義了一個簡單的 bean 類 HolderBean
和一個訪問類 ReflectAccess
。訪問類有一個命令列引數,該引數必須是一個值為 int
的
bean 類屬性的名字( value1
或者 value2
)。它增加指定屬性的值,然後在退出前打印出這兩個屬性值。
清單 1. 反射一個 bean
public class HolderBean { private int m_value1; private int m_value2; public int getValue1() { return m_value1; } public void setValue1(int value) { m_value1 = value; } public int getValue2() { return m_value2; } public void setValue2(int value) { m_value2 = value; } } public class ReflectAccess { public void run(String[] args) throws Exception { if (args.length == 1 && args[0].length() > 0) { // create property name char lead = args[0].charAt(0); String pname = Character.toUpperCase(lead) + args[0].substring(1); // look up the get and set methods Method gmeth = HolderBean.class.getDeclaredMethod ("get" + pname, new Class[0]); Method smeth = HolderBean.class.getDeclaredMethod ("set" + pname, new Class[] { int.class }); // increment value using reflection HolderBean bean = new HolderBean(); Object start = gmeth.invoke(bean, null); int incr = ((Integer)start).intValue() + 1; smeth.invoke(bean, new Object[] {new Integer(incr)}); // print the ending values System.out.println("Result values " + bean.getValue1() + ", " + bean.getValue2()); } else { System.out.println("Usage: ReflectAccess value1|value2"); } } }
下面是執行 ReflectAccess
的兩個例子,用來展示結果:
[dennis]$ java -cp . ReflectAccess value1 Result values 1, 0 [dennis]$ java -cp . ReflectAccess value2 Result values 0, 1
構建 glue 類
我已經展示了反射版本的程式碼,現在要展示如何用生成的類來取代反射。要想讓這種取代可以正確工作,會涉及到一個微妙的問題,它可追溯到本系列 第 1 部分中對類裝載的討論。這個問題是:我想要在執行時生成一個可從訪問類的靜態編譯的程式碼進行訪問的類,但是因為對編譯器來說生成的類不存在,因此沒辦法直接引用它。
不要錯過本系列的其他內容
第 1 部分,“ 類和類裝入”(2003 年 4 月)
第 2 部分,“ 引入反射” (2003 年 6 月)
第 3 部分," 應用反射" (2003 年 7 月)
第 5 部分,“ 動態轉換類” (2004 年 2 月)
第 7 部分,“ 用 BCEL 設計位元組碼” (2004 年 4 月)
那麼如何將靜態編譯的程式碼連結到生成的類呢?基本的解決方案是定義可以用靜態編譯的程式碼訪問的基類或者介面,然後生成的類擴充套件這個基類或者實現這個介面。這樣靜態編譯的程式碼就可以直接呼叫方法,即使方法只有到了執行時才能真正實現。
在清單 2 中,我定義了一個介面 IAccess
,目的是為生成的程式碼提供這種連結。這個介面包括三個方法。第一個方法只是設定要訪問的目標物件。另外兩個方法是用於訪問一個 int
屬性值的
get 和 set 方法的代理。
清單 2. 到 glue 類的介面
public interface IAccess { public void setTarget(Object target); public int getValue(); public void setValue(int value); }
這裡的意圖是讓 IAccess
介面的生成實現提供呼叫目標類的相應
get 和 set 方法的程式碼。清單 3 顯示了實現這個介面的一個例子,假定我希望訪問 清單
1 中 HolderBean
類的 value1
屬性:
清單 3. Glue 類示例實現
public class AccessValue1 implements IAccess { private HolderBean m_target; public void setTarget(Object target) { m_target = (HolderBean)target; } public int getValue() { return m_target.getValue1(); } public void setValue(int value) { m_target.setValue1(value); } }
清單 2 介面設計為針對特定型別物件的特定屬性使用。這個介面使實現程式碼簡單了 —— 在處理位元組碼時這總是一個優點 —— 但是也意味著實現類是非常特定的。對於要通過這個介面訪問的每一種型別的物件和屬性,都需要一個單獨的實現類,這限制了將這種方法作為反射的一般性替代方法。 如果選擇只在反射效能真正成為瓶頸的情況下才使用這種技術,那麼這種限制就不是一個問題。
用 Javassist 生成
用 Javassist 為 清單 2 IAccess
介面生成實現類很容易
—— 只需要建立一個實現了這個介面的新類、為目標物件引用新增一個成員變數、最後再新增一個無參建構函式和簡單實現方法。清單 4 顯示了完成這些步驟的 Javassist 程式碼,它構造一個方法呼叫,這個方法以目標類和 get/set 方法資訊為引數、並返回所構造的類的二進位制表示:
清單 4. Javassist glue 類構造
/** Parameter types for call with no parameters. */ private static final CtClass[] NO_ARGS = {}; /** Parameter types for call with single int value. */ private static final CtClass[] INT_ARGS = { CtClass.intType }; protected byte[] createAccess(Class tclas, Method gmeth, Method smeth, String cname) throws Exception { // build generator for the new class String tname = tclas.getName(); ClassPool pool = ClassPool.getDefault(); CtClass clas = pool.makeClass(cname); clas.addInterface(pool.get("IAccess")); CtClass target = pool.get(tname); // add target object field to class CtField field = new CtField(target, "m_target", clas); clas.addField(field); // add public default constructor method to class CtConstructor cons = new CtConstructor(NO_ARGS, clas); cons.setBody(";"); clas.addConstructor(cons); // add public setTarget method CtMethod meth = new CtMethod(CtClass.voidType, "setTarget", new CtClass[] { pool.get("java.lang.Object") }, clas); meth.setBody("m_target = (" + tclas.getName() + ")$1;"); clas.addMethod(meth); // add public getValue method meth = new CtMethod(CtClass.intType, "getValue", NO_ARGS, clas); meth.setBody("return m_target." + gmeth.getName() + "();"); clas.addMethod(meth); // add public setValue method meth = new CtMethod(CtClass.voidType, "setValue", INT_ARGS, clas); meth.setBody("m_target." + smeth.getName() + "($1);"); clas.addMethod(meth); // return binary representation of completed class return clas.toBytecode(); }
我不準備詳細討論這些程式碼,因為如果您一直跟著學習本系列,這裡的大多數操作都是所熟悉的(如果您 還沒有 看過本系列,請現在閱讀 第 5 部分,瞭解使用 Javassist 的概述)。
用 BCEL 生成
用 BCEL 生成 清單 2 IAccess
的實現類不像使用
Javassist 那樣容易,但是也不是很複雜。清單 5 給出了相應的程式碼。 這段程式碼使用與清單 4 Javassist 程式碼相同的一組操作,但是執行時間要長一些,因為需要為 BCEL 拼出每一個位元組碼指令。與使用 Javassist 時一樣,我將跳過實現的細節(如果有不熟悉的地方,請參閱 第
7 部分對 BCEL 的概述)。
清單 5. BCEL glue 類構造
/** Parameter types for call with single int value. */ private static final Type[] INT_ARGS = { Type.INT }; /** Utility method for adding constructed method to class. */ private static void addMethod(MethodGen mgen, ClassGen cgen) { mgen.setMaxStack(); mgen.setMaxLocals(); InstructionList ilist = mgen.getInstructionList(); Method method = mgen.getMethod(); ilist.dispose(); cgen.addMethod(method); } protected byte[] createAccess(Class tclas, java.lang.reflect.Method gmeth, java.lang.reflect.Method smeth, String cname) { // build generators for the new class String tname = tclas.getName(); ClassGen cgen = new ClassGen(cname, "java.lang.Object", cname + ".java", Constants.ACC_PUBLIC, new String[] { "IAccess" }); InstructionFactory ifact = new InstructionFactory(cgen); ConstantPoolGen pgen = cgen.getConstantPool(); //. add target object field to class FieldGen fgen = new FieldGen(Constants.ACC_PRIVATE, new ObjectType(tname), "m_target", pgen); cgen.addField(fgen.getField()); int findex = pgen.addFieldref(cname, "m_target", Utility.getSignature(tname)); // create instruction list for default constructor InstructionList ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(ifact.createInvoke("java.lang.Object", "<init>", Type.VOID, Type.NO_ARGS, Constants.INVOKESPECIAL)); ilist.append(InstructionFactory.createReturn(Type.VOID)); // add public default constructor method to class MethodGen mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, Type.NO_ARGS, null, "<init>", cname, ilist, pgen); addMethod(mgen, cgen); // create instruction list for setTarget method ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(InstructionConstants.ALOAD_1); ilist.append(new CHECKCAST(pgen.addClass(tname))); ilist.append(new PUTFIELD(findex)); ilist.append(InstructionConstants.RETURN); // add public setTarget method mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, new Type[] { Type.OBJECT }, null, "setTarget", cname, ilist, pgen); addMethod(mgen, cgen); // create instruction list for getValue method ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(new GETFIELD(findex)); ilist.append(ifact.createInvoke(tname, gmeth.getName(), Type.INT, Type.NO_ARGS, Constants.INVOKEVIRTUAL)); ilist.append(InstructionConstants.IRETURN); // add public getValue method mgen = new MethodGen(Constants.ACC_PUBLIC, Type.INT, Type.NO_ARGS, null, "getValue", cname, ilist, pgen); addMethod(mgen, cgen); // create instruction list for setValue method ilist = new InstructionList(); ilist.append(InstructionConstants.ALOAD_0); ilist.append(new GETFIELD(findex)); ilist.append(InstructionConstants.ILOAD_1); ilist.append(ifact.createInvoke(tname, smeth.getName(), Type.VOID, INT_ARGS, Constants.INVOKEVIRTUAL)); ilist.append(InstructionConstants.RETURN); // add public setValue method mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID, INT_ARGS, null, "setValue", cname, ilist, pgen); addMethod(mgen, cgen); // return bytecode of completed class return cgen.getJavaClass().getBytes(); }
效能檢查
已經介紹了 Javassist 和 BCEL 版本的方法構造,現在可以試用它們以瞭解它們工作的情況。在執行時生成程式碼的根本理由是用一些更快的的東西取代反射,所以最好加入效能比較以瞭解在這方面的改進。為了更加有趣,我還將比較用兩種框架構造 glue 類所用的時間。
清單 6 顯示用於檢查效能的測試程式碼的主要部分。 runReflection()
方法執行測試的反射部分, runAccess()
執行直接訪問部分, run()
控制整個程序(包括列印時間結果)。 runReflection()
和 runAccess()
都取要執行的次數作為引數,這個引數是以命令列的形式傳遞的(使用的程式碼沒有在清單中顯示,但是包括在下載中)。 DirectLoader
類(在清單
6 的結尾)只提供了裝載生成的類的一種容易的方式。
清單 6. 效能測試程式碼
/** Run timed loop using reflection for access to value. */ private int runReflection(int num, Method gmeth, Method smeth, Object obj) { int value = 0; try { Object[] gargs = new Object[0]; Object[] sargs = new Object[1]; for (int i = 0; i < num; i++) { // messy usage of Integer values required in loop Object result = gmeth.invoke(obj, gargs); value = ((Integer)result).intValue() + 1; sargs[0] = new Integer(value); smeth.invoke(obj, sargs); } } catch (Exception ex) { ex.printStackTrace(System.err); System.exit(1); } return value; } /** Run timed loop using generated class for access to value. */ private int runAccess(int num, IAccess access, Object obj) { access.setTarget(obj); int value = 0; for (int i = 0; i < num; i++) { value = access.getValue() + 1; access.setValue(value); } return value; } public void run(String name, int count) throws Exception { // get instance and access methods HolderBean bean = new HolderBean(); String pname = name; char lead = pname.charAt(0); pname = Character.toUpperCase(lead) + pname.substring(1); Method gmeth = null; Method smeth = null; try { gmeth = HolderBean.class.getDeclaredMethod("get" + pname, new Class[0]); smeth = HolderBean.class.getDeclaredMethod("set" + pname, new Class[] { int.class }); } catch (Exception ex) { System.err.println("No methods found for property " + pname); ex.printStackTrace(System.err); return; } // create the access class as a byte array long base = System.currentTimeMillis(); String cname = "IAccess$impl_HolderBean_" + gmeth.getName() + "_" + smeth.getName(); byte[] bytes = createAccess(HolderBean.class, gmeth, smeth, cname); // load and construct an instance of the class Class clas = s_classLoader.load(cname, bytes); IAccess access = null; try { access = (IAccess)clas.newInstance(); } catch (IllegalAccessException ex) { ex.printStackTrace(System.err); System.exit(1); } catch (InstantiationException ex) { ex.printStackTrace(System.err); System.exit(1); } System.out.println("Generate and load time of " + (System.currentTimeMillis()-base) + " ms."); // run the timing comparison long start = System.currentTimeMillis(); int result = runReflection(count, gmeth, smeth, bean); long time = System.currentTimeMillis() - start; System.out.println("Reflection took " + time + " ms. with result " + result + " (" + bean.getValue1() + ", " + bean.getValue2() + ")"); bean.setValue1(0); bean.setValue2(0); start = System.currentTimeMillis(); result = runAccess(count, access, bean); time = System.currentTimeMillis() - start; System.out.println("Generated took " + time + " ms. with result " + result + " (" + bean.getValue1() + ", " + bean.getValue2() + ")"); } /** Simple-minded loader for constructed classes. */ protected static class DirectLoader extends SecureClassLoader { protected DirectLoader() { super(TimeCalls.class.getClassLoader()); } protected Class load(String name, byte[] data) { return super.defineClass(name, data, 0, data.length); } }
為了進行簡單的計時測試,我呼叫 run()
方法兩次,對於 清單
1 HolderBean
類中的每個屬性呼叫一次。執行兩次測試對於測試的公正性是很重要的
—— 第一次執行程式碼要裝載所有必要的類,這對於 Javassist 和 BCEL 類生成過程都會增加大量開銷。不過,在第二次執行時不需要這種開銷,這樣就能更好地估計在實際的系統中使用時,類生成需要多長的時間。 下面是一個執行測試時生成的示例輸出:
[dennis]$$ java -cp .:bcel.jar BCELCalls 2000 Generate and load time of 409 ms. Reflection took 61 ms. with result 2000 (2000, 0) Generated took 2 ms. with result 2000 (2000, 0) Generate and load time of 1 ms. Reflection took 13 ms. with result 2000 (0, 2000) Generated took 2 ms. with result 2000 (0, 2000)
圖 1 顯示了用從 2k 到 512k 次迴圈進行呼叫時計時測試的結果(在執行 Mandrake Linux 9.1 的 Athlon 2200+ XP 系統上執行測試,使用 Sun 1.4.2 JVM )。這裡,我在每次測試執行中加入了第二個屬性的反射時間和生成的程式碼的時間(這樣首先是使用 Javassist 程式碼生成的兩個時間,然後是使用 BCEL 程式碼生成時的同樣兩個時間)。不管是用 Javassist 還是 BCEL 生成 glue 類,執行時間大致是相同的,這也是我預計的結果 —— 但是確認一下總是好的!
圖 1. 反射速度與生成的程式碼的速度(時間單位為毫秒)
從圖 1 中可以看出,不管在什麼情況下,生成的程式碼執行都比反射要快得多。生成的程式碼的速度優勢隨著迴圈次數的增加而增加,在 2k 次迴圈時大約為 5:1,在 512K 次迴圈時增加到大約 24:1。對於 Javassist,構造並裝載第一個 glue 類需要大約 320 毫秒(ms),而對於 BCEL 則為 370 ms,而構造第二個 glue 類對於 Javassist 只用 4 ms,對於 BCEL 只用 2 ms(由於時鐘解析度只有 1ms,因此這些時間是非常粗略的)。如果將這些時間結合到一起,將會看到即使對於 2k 次迴圈,生成一個類也比使用反射有更好的整體效能(總執行時間為約 4 ms 到 6 ms,而使用反射時大約為 14 ms)。
此外,實際情況比這個圖中所表明的更有利於生成的程式碼。在迴圈減少至 25 次迴圈時,反射程式碼的執行仍然要用 6 ms 到 7 ms,而生成的程式碼執行得太快以致不能記錄。針對相對較少的迴圈次數,反射所花的時間反映出,當達到一個閾值時在 JVM 中進行了某種優化,如果我將迴圈次數降低到少於 20,那麼反射程式碼也會快得無法記錄。
加速上路
現在已經看到了執行時 classworking 可以為應用程式帶來什麼樣的效能。下次面臨難處理的效能優化問題時要記住它 —— 它可能就是避免大的重新設計的關鍵所在。不過,Classworking 不僅有效能上的有好處,它還是一種使應用程式適合執行時要求的靈活方式。即使沒有理由在程式碼中使用它,我也認為它是使程式設計變得有趣的一種 Java 功能。
對一個 classworking 的真實世界應用程式的探討結束了“Java 程式設計的動態性”這一系列。但是不要失望 —— 當我展示一些為操縱 Java 位元組碼而構建的工具時,您很快就有機會在 developerWorks 中瞭解一些其他的 classworking 應用程式了。首先將是一篇關於 Mother Goose直接推出的兩個測試工具的文章。