Java 编程的动态性,第 8 部分: 用代码生成取代反射--转载
既然您已經(jīng)看到了如何使用 Javassist 和 BCEL 框架來進(jìn)行 classworking (請參閱?本系列以前的一組文章), 我將展示一個(gè)實(shí)際的 classworking 應(yīng)用程序。這個(gè)應(yīng)用程序用運(yùn)行時(shí)生成的、并立即裝載到 JVM 的類來取代反射。在綜合討論的過程中,我將引用本系列的前兩篇文章,以及對 Javassist 和 BCEL 的討論,這樣本文就成為了對這個(gè)很長的系列文章的一個(gè)很好的總結(jié)。
反射的性能
在?第 2 部分, 我展示了無論是對于字段訪問還是方法調(diào)用,反射都比直接代碼慢很多倍。這種延緩對于許多應(yīng)用程序來說不算是問題,但是總是會(huì)遇到性能非常關(guān)鍵的情況。在這種情況下,反射可能成為真正的瓶頸。但是,用靜態(tài)編譯的代碼取代反射可能會(huì)非常混亂,并且在有些情況下(如在這種框架中:反射訪問的類或者項(xiàng)目是在運(yùn)行時(shí)提供的,而不是作為這一編譯過程的一部分提供的),如果不重新構(gòu)建整個(gè)應(yīng)用程序就根本不可能取代。
Classworking 使我們有機(jī)會(huì)將靜態(tài)編譯的代碼的性能與反射的靈活性結(jié)合起來。這里的基本方法是,在運(yùn)行時(shí),以一種可以被一般性代碼使用的方式,構(gòu)建一個(gè)自定義的類,其中將包裝對目標(biāo)類的訪問(以前是通過反射達(dá)到的)。將這個(gè)自定義類裝載到 JVM 中后,就可以全速運(yùn)行了。
設(shè)置階段
清單 1 給出了應(yīng)用程序的起點(diǎn)。這里定義了一個(gè)簡單的 bean 類?HolderBean?和一個(gè)訪問類?ReflectAccess?。訪問類有一個(gè)命令行參數(shù),該參數(shù)必須是一個(gè)值為?int?的 bean 類屬性的名字(?value1?或者?value2?)。它增加指定屬性的值,然后在退出前打印出這兩個(gè)屬性值。
清單 1. 反射一個(gè) 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 namechar lead = args[0].charAt(0);String pname = Character.toUpperCase(lead) +args[0].substring(1);// look up the get and set methodsMethod gmeth = HolderBean.class.getDeclaredMethod("get" + pname, new Class[0]);Method smeth = HolderBean.class.getDeclaredMethod("set" + pname, new Class[] { int.class });// increment value using reflectionHolderBean 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 valuesSystem.out.println("Result values " +bean.getValue1() + ", " + bean.getValue2());} else {System.out.println("Usage: ReflectAccess value1|value2");}} }下面是運(yùn)行?ReflectAccess?的兩個(gè)例子,用來展示結(jié)果:
[dennis]$ java -cp . ReflectAccess value1 Result values 1, 0 [dennis]$ java -cp . ReflectAccess value2 Result values 0, 1構(gòu)建 glue 類
我已經(jīng)展示了反射版本的代碼,現(xiàn)在要展示如何用生成的類來取代反射。要想讓這種取代可以正確工作,會(huì)涉及到一個(gè)微妙的問題,它可追溯到本系列?第 1 部分中對類裝載的討論。這個(gè)問題是:我想要在運(yùn)行時(shí)生成一個(gè)可從訪問類的靜態(tài)編譯的代碼進(jìn)行訪問的類,但是因?yàn)閷幾g器來說生成的類不存在,因此沒辦法直接引用它。
那么如何將靜態(tài)編譯的代碼鏈接到生成的類呢?基本的解決方案是定義可以用靜態(tài)編譯的代碼訪問的基類或者接口,然后生成的類擴(kuò)展這個(gè)基類或者實(shí)現(xiàn)這個(gè)接口。這樣靜態(tài)編譯的代碼就可以直接調(diào)用方法,即使方法只有到了運(yùn)行時(shí)才能真正實(shí)現(xiàn)。
在清單 2 中,我定義了一個(gè)接口?IAccess?,目的是為生成的代碼提供這種鏈接。這個(gè)接口包括三個(gè)方法。第一個(gè)方法只是設(shè)置要訪問的目標(biāo)對象。另外兩個(gè)方法是用于訪問一個(gè)?int?屬性值的 get 和 set 方法的代理。
清單 2. 到 glue 類的接口
public interface IAccess {public void setTarget(Object target);public int getValue();public void setValue(int value); }這里的意圖是讓?IAccess?接口的生成實(shí)現(xiàn)提供調(diào)用目標(biāo)類的相應(yīng) get 和 set 方法的代碼。清單 3 顯示了實(shí)現(xiàn)這個(gè)接口的一個(gè)例子,假定我希望訪問?清單 1?中 HolderBean?類的?value1?屬性:
清單 3. Glue 類示例實(shí)現(xiàn)
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?接口設(shè)計(jì)為針對特定類型對象的特定屬性使用。這個(gè)接口使實(shí)現(xiàn)代碼簡單了 —— 在處理字節(jié)碼時(shí)這總是一個(gè)優(yōu)點(diǎn) —— 但是也意味著實(shí)現(xiàn)類是非常特定的。對于要通過這個(gè)接口訪問的每一種類型的對象和屬性,都需要一個(gè)單獨(dú)的實(shí)現(xiàn)類,這限制了將這種方法作為反射的一般性替代方法。 如果選擇只在反射性能真正成為瓶頸的情況下才使用這種技術(shù),那么這種限制就不是一個(gè)問題。
用 Javassist 生成
用 Javassist 為?清單 2?IAccess?接口生成實(shí)現(xiàn)類很容易 —— 只需要?jiǎng)?chuàng)建一個(gè)實(shí)現(xiàn)了這個(gè)接口的新類、為目標(biāo)對象引用添加一個(gè)成員變量、最后再添加一個(gè)無參構(gòu)造函數(shù)和簡單實(shí)現(xiàn)方法。清單 4 顯示了完成這些步驟的 Javassist 代碼,它構(gòu)造一個(gè)方法調(diào)用,這個(gè)方法以目標(biāo)類和 get/set 方法信息為參數(shù)、并返回所構(gòu)造的類的二進(jìn)制表示:
清單 4. Javassist glue 類構(gòu)造
/** 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 classString 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 classCtField field = new CtField(target, "m_target", clas);clas.addField(field);// add public default constructor method to classCtConstructor cons = new CtConstructor(NO_ARGS, clas);cons.setBody(";");clas.addConstructor(cons);// add public setTarget methodCtMethod 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 methodmeth = new CtMethod(CtClass.intType, "getValue", NO_ARGS, clas);meth.setBody("return m_target." + gmeth.getName() + "();");clas.addMethod(meth);// add public setValue methodmeth = new CtMethod(CtClass.voidType, "setValue", INT_ARGS, clas);meth.setBody("m_target." + smeth.getName() + "($1);");clas.addMethod(meth);// return binary representation of completed classreturn clas.toBytecode(); }我不準(zhǔn)備詳細(xì)討論這些代碼,因?yàn)槿绻恢备鴮W(xué)習(xí)本系列,這里的大多數(shù)操作都是所熟悉的(如果您?還沒有?看過本系列,請現(xiàn)在閱讀?第 5 部分,了解使用 Javassist 的概述)。
用 BCEL 生成
用 BCEL 生成?清單 2?IAccess?的實(shí)現(xiàn)類不像使用 Javassist 那樣容易,但是也不是很復(fù)雜。清單 5 給出了相應(yīng)的代碼。 這段代碼使用與清單 4 Javassist 代碼相同的一組操作,但是運(yùn)行時(shí)間要長一些,因?yàn)樾枰獮?BCEL 拼出每一個(gè)字節(jié)碼指令。與使用 Javassist 時(shí)一樣,我將跳過實(shí)現(xiàn)的細(xì)節(jié)(如果有不熟悉的地方,請參閱?第 7 部分對 BCEL 的概述)。
清單 5. BCEL glue 類構(gòu)造
/** 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 classString 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 classFieldGen 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 constructorInstructionList 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 classMethodGen mgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID,Type.NO_ARGS, null, "<init>", cname, ilist, pgen);addMethod(mgen, cgen);// create instruction list for setTarget methodilist = 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 methodmgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID,new Type[] { Type.OBJECT }, null, "setTarget", cname,ilist, pgen);addMethod(mgen, cgen);// create instruction list for getValue methodilist = 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 methodmgen = new MethodGen(Constants.ACC_PUBLIC, Type.INT,Type.NO_ARGS, null, "getValue", cname, ilist, pgen);addMethod(mgen, cgen);// create instruction list for setValue methodilist = 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 methodmgen = new MethodGen(Constants.ACC_PUBLIC, Type.VOID,INT_ARGS, null, "setValue", cname, ilist, pgen);addMethod(mgen, cgen);// return bytecode of completed classreturn cgen.getJavaClass().getBytes(); }性能檢查
已經(jīng)介紹了 Javassist 和 BCEL 版本的方法構(gòu)造,現(xiàn)在可以試用它們以了解它們工作的情況。在運(yùn)行時(shí)生成代碼的根本理由是用一些更快的的東西取代反射,所以最好加入性能比較以了解在這方面的改進(jìn)。為了更加有趣,我還將比較用兩種框架構(gòu)造 glue 類所用的時(shí)間。
清單 6 顯示用于檢查性能的測試代碼的主要部分。?runReflection()?方法運(yùn)行測試的反射部分,?runAccess()?運(yùn)行直接訪問部分,?run()?控制整個(gè)進(jìn)程(包括打印時(shí)間結(jié)果)。?runReflection()?和?runAccess()?都取要執(zhí)行的次數(shù)作為參數(shù),這個(gè)參數(shù)是以命令行的形式傳遞的(使用的代碼沒有在清單中顯示,但是包括在下載中)。?DirectLoader?類(在清單 6 的結(jié)尾)只提供了裝載生成的類的一種容易的方式。
清單 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 loopObject 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 methodsHolderBean 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 arraylong 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 classClass 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 comparisonlong 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);} }為了進(jìn)行簡單的計(jì)時(shí)測試,我調(diào)用?run()?方法兩次,對于?清單 1?HolderBean?類中的每個(gè)屬性調(diào)用一次。運(yùn)行兩次測試對于測試的公正性是很重要的 —— 第一次運(yùn)行代碼要裝載所有必要的類,這對于 Javassist 和 BCEL 類生成過程都會(huì)增加大量開銷。不過,在第二次運(yùn)行時(shí)不需要這種開銷,這樣就能更好地估計(jì)在實(shí)際的系統(tǒng)中使用時(shí),類生成需要多長的時(shí)間。 下面是一個(gè)執(zhí)行測試時(shí)生成的示例輸出:
[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 次循環(huán)進(jìn)行調(diào)用時(shí)計(jì)時(shí)測試的結(jié)果(在運(yùn)行 Mandrake Linux 9.1 的 Athlon 2200+ XP 系統(tǒng)上運(yùn)行測試,使用 Sun 1.4.2 JVM )。這里,我在每次測試運(yùn)行中加入了第二個(gè)屬性的反射時(shí)間和生成的代碼的時(shí)間(這樣首先是使用 Javassist 代碼生成的兩個(gè)時(shí)間,然后是使用 BCEL 代碼生成時(shí)的同樣兩個(gè)時(shí)間)。不管是用 Javassist 還是 BCEL 生成 glue 類,執(zhí)行時(shí)間大致是相同的,這也是我預(yù)計(jì)的結(jié)果 —— 但是確認(rèn)一下總是好的!
圖 1. 反射速度與生成的代碼的速度(時(shí)間單位為毫秒)
從圖 1 中可以看出,不管在什么情況下,生成的代碼執(zhí)行都比反射要快得多。生成的代碼的速度優(yōu)勢隨著循環(huán)次數(shù)的增加而增加,在 2k 次循環(huán)時(shí)大約為 5:1,在 512K 次循環(huán)時(shí)增加到大約 24:1。對于 Javassist,構(gòu)造并裝載第一個(gè) glue 類需要大約 320 毫秒(ms),而對于 BCEL 則為 370 ms,而構(gòu)造第二個(gè) glue 類對于 Javassist 只用 4 ms,對于 BCEL 只用 2 ms(由于時(shí)鐘分辨率只有 1ms,因此這些時(shí)間是非常粗略的)。如果將這些時(shí)間結(jié)合到一起,將會(huì)看到即使對于 2k 次循環(huán),生成一個(gè)類也比使用反射有更好的整體性能(總執(zhí)行時(shí)間為約 4 ms 到 6 ms,而使用反射時(shí)大約為 14 ms)。
此外,實(shí)際情況比這個(gè)圖中所表明的更有利于生成的代碼。在循環(huán)減少至 25 次循環(huán)時(shí),反射代碼的執(zhí)行仍然要用 6 ms 到 7 ms,而生成的代碼運(yùn)行得太快以致不能記錄。針對相對較少的循環(huán)次數(shù),反射所花的時(shí)間反映出,當(dāng)達(dá)到一個(gè)閾值時(shí)在 JVM 中進(jìn)行了某種優(yōu)化,如果我將循環(huán)次數(shù)降低到少于 20,那么反射代碼也會(huì)快得無法記錄。
加速上路
現(xiàn)在已經(jīng)看到了運(yùn)行時(shí) classworking 可以為應(yīng)用程序帶來什么樣的性能。下次面臨難處理的性能優(yōu)化問題時(shí)要記住它 —— 它可能就是避免大的重新設(shè)計(jì)的關(guān)鍵所在。不過,Classworking 不僅有性能上的有好處,它還是一種使應(yīng)用程序適合運(yùn)行時(shí)要求的靈活方式。即使沒有理由在代碼中使用它,我也認(rèn)為它是使編程變得有趣的一種 Java 功能。
對一個(gè) classworking 的真實(shí)世界應(yīng)用程序的探討結(jié)束了“Java 編程的動(dòng)態(tài)性”這一系列。但是不要失望 —— 當(dāng)我展示一些為操縱 Java 字節(jié)碼而構(gòu)建的工具時(shí),您很快就有機(jī)會(huì)在?developerWorks?中了解一些其他的 classworking 應(yīng)用程序了。首先將是一篇關(guān)于?Mother Goose直接推出的兩個(gè)測試工具的文章。
原文:http://www.ibm.com/developerworks/cn/java/j-dyn0610/index.html
?
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/4036129.html
總結(jié)
以上是生活随笔為你收集整理的Java 编程的动态性,第 8 部分: 用代码生成取代反射--转载的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 编程的动态性,第 7 部分:
- 下一篇: 基于 Java 2 运行时安全模型的线程