程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java編程的動態性,第8部分: 用代碼生成取代反射

Java編程的動態性,第8部分: 用代碼生成取代反射

編輯:關於JAVA

從本系列前面的文章中,您了解到反射的性能比直接訪問要慢許多倍,並了解了用 Javassist 和 Apache Byte Code Engineering Library (BCEL)進行classworking。Java 顧問 Dennis Sosnoski 通過演示如何使用運行時 classworking,來用全速前進的生成代碼 取代反射代碼,從而結束他的 Java 編程的動態性系列。

既然您已經看到了如何使用 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 部分中對類裝載的討論 。這個問題是:我想要在運行時生成一個可從訪問類的靜態編譯的代碼進行訪問的類,但是 因為對編譯器來說生成的類不存在,因此沒辦法直接引用它。

那麼如何將靜態編譯的代碼鏈接到生成的類呢?基本的解決方案是定義可以用靜態編譯的 代碼訪問的基類或者接口,然後生成的類擴展這個基類或者實現這個接口。這樣靜態編譯的 代碼就可以直接調用方法,即使方法只有到了運行時才能真正實現。

在清單 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直接推出的兩個測試工具的文章。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved