程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Classworking 工具箱: ASM classworking

Classworking 工具箱: ASM classworking

編輯:關於JAVA

ASM classworking 庫聲稱自己又小又快是否名符其實?用 ASM 2.0 測試一下就知道。

簡介:在這一期的 Classworking 工具箱中,咨詢顧問 Dennis Sosnoski 把 ASM 字節碼操作框架與 他以前在 Java 編程動態性系列中討論過的字節碼工程庫(Byte Code Engineering Library,BCEL)以 及 Javassist 框架進行比較。ASM 聲稱自己又小又快 —— 但將它與其他框架進行比較的情況如何樣呢 ? Dennis 將采用他在以前系列文章中使用的示例對 ASM 的可用性和性能進行評估。

目前已經開發了若干個處理字節碼和類文件的 Java 庫,其中包括我在以前的 Java 編程動態性 系列 中介紹的 Javassist 和 BCEL 庫。ASM 是這種類型的另一個更新的庫。與其他庫不同,ASM 被設計和實 現為盡可能小而快。在本月的這一期文章中,我將深入研究 ASM 在這一點上做得到底如何,將它與其他 兩個用作本系列中的示例的庫進行比較。

在上一期文章中,我演示了如何用運行時字節碼生成來代替反射。那次,我使用了 1.4.1 的 JVM 進 行測試,結果發現,生成的代碼運行起來可能要比它替換的反射代碼更快。除了在 ASM 上采用同樣的手 段進行測試之外,在這一期中,我還更新了結果,用 1.5.0 的 JVM 進行測試,看看 1.5.0 中實現的性 能增強是否會改變結果。

代替反射

示例應用程序的目的是用運行時生成的代碼代替反射。 在我的 Java 編程動態性 系列中,我已經深入介紹過這個主題。在這一期的文章中,我將對以前的材料 做一個快速的背景總結,然後看看在使用 ASM 代替 Javassist 和 BCEL 框架時,與這兩者相比,ASM 的 性能和可用性如何。

設置階段

反射為在運行時訪問對象和元數據提供了非常強大的機制( 正如我在“Java 編程動態性,第 2 部分” 中討論過的)。使用反射使構建應用程序更加靈 活,可以在運行時用外部信息把各個片斷掛接(hook)在一起,形成一個工作配置。但是利用反射來實際 訪問對象通常比直接執行相同的操作慢得多。使用基於反射的方法構建應用程序,而後發現需要改進性能 ,這樣會帶來真正的問題,因為反射支持的靈活性很難以其他方式做到。

Classworking 技術提供 了一種方法。它沒有使用反射來訪問對象的屬性,例如,可以在運行時構建一個類來做同樣的事 —— 但這樣做會快許多。“Java 編程動態性,第 8 部分”演示了如何用 Javassist 和 BCEL 這兩個 classworking 框架來實現這種類型的反射替代。這篇文章采用的基本原則很 簡單:首先創建一個接口(該接口定義所需的函數),然後在運行時構建一個類(該類實現前面的接口, 並把函數掛接到目標對象上)。

清單 1 演示了這種方法。在這裡,HolderBean 類包含一對屬性 ,通過使用反射來調用 get 和 set 方法,可以在運行時訪問這一對屬性。IAccess 接口抽象化了通過 get 和 set 方法訪問 int 值屬性的概念,而 AccessValue1 類則特別針對 HolderBean 類的 “value1”屬性給出了這個接口的實現。

清單 1. 反射替代接口和實現

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 interface IAccess
{
  public void  setTarget(Object target);
  public int getValue();
  public void  setValue(int value);
}
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);
  }
}

如果 不得不手工編碼諸如清單 1 中的 AccessValue1 那樣的每個實現類,那麼整個方法可能都不是很有用。 但是 AccessValue1 中的代碼非常簡單,這使它成為運行時類生成的理想目標。可以使用 AccessValue1 字節碼作為模板,以生成特定於具體目標對象類型和 get/set 方法對的類,只要用這些目標替換掉 AccessValue1 中使用的那些目標即可。這是我在以前的文章中使用的方法,也是我在這一期中用在 ASM 上的方法。

使用 ASM

我在前面的文章中介紹的兩個 classworking 框架采用了截然不同的 兩種方法來處理字節碼。Javassist 使用 Java 源代碼的簡化版本,然後再把代碼編譯成字節碼。這讓 Javassist 很容易使用,但是這也把字節碼的使用范圍限制在了 Javassist 源代碼的限制使用范圍中。 另一方面,BCEL 直接處理字節碼。BCEL 提供了操縱字節碼指令的結構和技術,把它從單純二進制值的級 別提高了一步,但是使用它要比使用 Javassist 難得多。

從操作級別上看,ASM 更靠近 BCEL 而 不是 Javassist,但是 ASM 采用了一種比 BCEL 更整潔的接口。原因之一在於 ASM 的基本設計。ASM 並 不直接操縱字節碼指令,而是采用 visitor 模式把類數據(包括指令序列) 當成事件流來處理。在解碼 現有類的時候,ASM 會為您生成事件流,並調用處理事件的方法。在生成新類的時候,這種處理方式就反 過來了 —— 您調用 ASM 類,它根據調用所表示的事件流構建新類。也可以使用雙向方法, 截住由現有類生成的事件流,做一些修改,並把修改後的事件流送回生成新類的事件流。

用 ASM 修改類

BCEL 和 ASM 都配備了能夠生成 Java 源代碼以編寫類的工具。這些工具背後的思路是: 可以將現有的類用作生成運行時類的模板。生成的源代碼包含重新生成模板類的二進制形式所必需的所有 調用,所以從理論上講,可以把這個代碼合並到應用程序代碼中,並修改它來滿足需要(例如,以參數的 形式替換那些需要在運行時修改的值)。

而在實踐中,我發現這種類編寫程序的 BCEL 版本 (org.apache.bcel.util.BCELifier)使用起來有一些限制。用於操縱指令列表的 BCEL 代碼很復雜,對 我來說,BCELifier 生成的源代碼太難看,無法使用。ASM 的類編寫程序也會產生一些難看的代碼,但是 只需稍做整理,它看起來就有用了。清單 2 顯示了在 清單 1 的 gen.AccessValue1 類上運行該程序 (org.objectweb.asm.util.ASMifierClassVisitor)所產生的結果。

清單 2. 從 gen.AccessValue1 生成的 ASM 代碼

package asm.gen;
import  org.objectweb.asm.*;
public class AccessValue1Dump implements Opcodes {
public static byte[] dump () throws Exception {
ClassWriter cw = new  ClassWriter(false);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor  av0;
cw.visit(V1_2, ACC_PUBLIC + ACC_SUPER, "gen/AccessValue1", null,  "java/lang/Object", new String[] 
 { "gen/IAccess" });
cw.visitSource ("AccessValue1.java", null);
{
fv = cw.visitField(0, "m_bean",  "Lgen/HolderBean;", null, null);
fv.visitEnd();
}
{
mv =  cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object",  "<init>", "()V");
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "setTarget",  "(Ljava/lang/Object;)V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD,  0);
mv.visitVarInsn(ALOAD, 1);
mv.visitTypeInsn(CHECKCAST, "gen/HolderBean");
mv.visitFieldInsn(PUTFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
{
mv  = cw.visitMethod(ACC_PUBLIC, "getValue", "()I", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "gen/AccessValue1",  "m_bean", "Lgen/HolderBean;");
mv.visitMethodInsn(INVOKEVIRTUAL, "gen/HolderBean",  "getValue1", "()I");
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC, "setValue", "(I)V",  null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "gen/AccessValue1", "m_bean", "Lgen/HolderBean;");
mv.visitVarInsn(ILOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "gen/HolderBean",  "setValue1", "(I)V");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();
}
cw.visitEnd();
return cw.toByteArray();
}
}

重新格式化之後,清單 2 的代碼就成為了我們下面要查看的反射替代代碼的基礎。

用 ASM 代替反射

“Java 編程動態性,第 8 部分” 采用了一個基類,對代碼 生成代替反射的不同實現進行測試,其中每個 classworking 庫都用獨立的子類來擴展基類。我將采用同 樣的方法來測試 ASM。

清單 3 給出了 ASM 實現的子類。反射替代類的構造是通過 createAccess() 方法完成的,該方法基於 清單 2 中 ASM 生成的代碼。清單 3 中的代碼與清單 2 中代 碼的主要區別是:我對清單 3 的格式和結構稍加了重新調整,而且還調整了目標類的參數,屬性的 get 和 set 方法,以及生成的類名稱,以便這個 ASM 版本的 createAccess() 方法與以前文章中使用的 Javassist 和 BCEL 版本兼容。

清單 3. ASM 測試類

public class ASMCalls  extends TimeCalls
{
  protected byte[] createAccess(Class tclas, Method  gmeth, Method smeth,
    String cname) throws Exception {

     // initialize writer for new class
    String ciname = cname.replace('.',  '/');
    ClassWriter cw = new ClassWriter(false);
    cw.visit (Opcodes.V1_2, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER,
      cname, null,  "java/lang/Object", new String[] { "gen/IAccess" });

    // add  field definition for reference to target class instance 
    String tiname  = Type.getInternalName(tclas);
    String ttype = "L" + tiname + ";";
    cw.visitField(0, "m_bean", ttype, null, null).visitEnd();

     // generate the default constructor
    MethodVisitor mv =
       cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
     mv.visitCode();
    mv.visitVarInsn(Opcodes.ALOAD, 0);
     mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
       "<init>", "()V");
    mv.visitInsn(Opcodes.RETURN);
     mv.visitMaxs(1, 1);
    mv.visitEnd();

    // generate the  setTarget method
    mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "setTarget",
      "(Ljava/lang/Object;)V", null, null);
    mv.visitCode();
     mv.visitVarInsn(Opcodes.ALOAD, 0);
    mv.visitVarInsn(Opcodes.ALOAD, 1);
    mv.visitTypeInsn(Opcodes.CHECKCAST, tiname);
    mv.visitFieldInsn (Opcodes.PUTFIELD, ciname, "m_bean", ttype);
    mv.visitInsn (Opcodes.RETURN);
    mv.visitMaxs(2, 2);
    mv.visitEnd();

    // generate the getValue method
    mv = cw.visitMethod (Opcodes.ACC_PUBLIC, "getValue", "()I", null, null);
    mv.visitCode();
    mv.visitVarInsn(Opcodes.ALOAD, 0);
    mv.visitFieldInsn(Opcodes.GETFIELD,  ciname, "m_bean", ttype);
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,  tiname,
      gmeth.getName(), "()I");
    mv.visitInsn (Opcodes.IRETURN);
    mv.visitMaxs(1, 1);
    mv.visitEnd();

    // generate the setValue method
    mv = cw.visitMethod (Opcodes.ACC_PUBLIC, "setValue", "(I)V", null, null);
    mv.visitCode();
    mv.visitVarInsn(Opcodes.ALOAD, 0);
    mv.visitFieldInsn(Opcodes.GETFIELD,  ciname, "m_bean", ttype);
    mv.visitVarInsn(Opcodes.ILOAD, 1);
     mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, tiname,
      smeth.getName(), "(I) V");
    mv.visitInsn(Opcodes.RETURN);
    mv.visitMaxs(2, 2);
     mv.visitEnd();

    // complete the class generation
     cw.visitEnd();
    return cw.toByteArray();
  }

  public  static void main(String[] args) throws Exception {
    if (args.length ==  1) {
      ASMCalls inst = new ASMCalls();
      inst.test(args [0]);
    } else {
      System.out.println("Usage: ASMCalls loop- count");
    }
  }
}

清單 3 的 createAccess() 代碼演示了使 用 ASM 的基本原則。我從創建 org.objectweb.asm.ClassWriter 開始, org.objectweb.asm.ClassWriter 接受類事件流(以方法調用的形式)並生成二進制類表示的輸出。我調 用編寫器的 visitField() 方法向構建的類添加一個字段,這將返回該字段的一個 visitor。返回的字段 visitor 可以用來為字段添加注釋或者特殊的屬性信息,但是在這個例子中,我不需要做什麼特殊的事情 ,可以只是立即調用字段 visitor 的 visitEnd() 方法。

在添加字段之後,我為構建的類添加了 4 個必要方法。清單 1 中的模板類源代碼是類的默認構造函數,第一個方法沒有出現在其中。這個構造 函數沒有參數,只是調用父類的構造函數,是在沒有指定類的構造函數時,由 Java 編譯器自動生成的。 因為我自己正在構建一個類,所以我需要顯式地創建默認構造函數。其余三個方法與 清單 1 源代碼中顯 示的方法相同。

在添加字段時,調用類編寫器的 visitMethod() 方法將為添加的方法返回一個 visitor。這個方法 visitor(org.objectweb.asm.MethodVisitor 接口的實例)可以用於為方法添加注 釋或特殊屬性,但是也為生成構成方法主體的實際字節碼指令序列提供了接口。清單 1 的代碼演示了如 何通過調用方法 visitor 來添加指令。在添加完所有指令後,就可以用最後一對調用來完成方法生成。 第一個調用是 visitMaxs(),它用於設置方法的最大堆棧大小和本地變量計數(這些值也可以由 ASM 自 動計算,並通過在調用中把 true 參數傳遞給 ClassWriter 構造函數對其進行配置)。最後一對調用中 的第二個調用是 visitEnd(),它只完成方法的構建過程。

一旦添加了字段和方法,獲得完成後的 類的二進制代碼就很容易。對類編寫器調用 visitEnd() 表明類編寫過程已經完成,而 toByteArray() 調用實際上返回的是二進制類映像。

檢測結果

在“Java 編程動態性,第 8 部分 ” 中,我展示了用 Javassist 和 BCEL 在運行時生成反射替代類所花費時間的計時結果,以及用 反射和替代類執行不同數量的訪問所花費時間的計時結果。在這一期中,我將展示同樣類型的結果,但稍 有變化。首先,我要把 ASM 包含在生成時間的比較中。我還要轉換到 JDK 1.5 中進行測試,以便能夠使 用 java.lang.System.nanoTime() 方法獲得更精確的計時結果。

圖 1 顯示了從 2k 到 51k 的循 環中,使用反射方法調用和生成類的時間的比較(測試是在一台 1GHz 的 PIIIm 筆記本上進行的,運行 的是 Mandrake Linux 10.0 系統,使用 Sun 的 1.5.0 JVM)。這些時間對於所有框架都是相同的。使用 生成代碼的性能優勢看起來並不像我在以前的測試中用 1.4.2 JVM 那麼好,但是它們仍然很有意義,因 為生成代碼運行起來要比反射快 10 到 14 倍。

圖 1. 反射和生成代碼速度對比(以毫秒為單位)

圖 1 的結果很有趣, 但是它們並不是本期的重點。關系更密切的是表 1 顯示的結果,它給出了使用每個框架來構建生成類所 花費的時間。在這裡,我為每個框架提供了兩個獨立的時間。第一個時間值是構建第一個反射替代類所花 費的時間,這個時間包括裝載和初始化框架代碼中的類的時間。後一個時間值是構建另外三個反射替代類 (針對其他屬性)的平均值。

表 1. 類的構建時間

框架 第一 個時間 第二個時間 Javassist 257 5.2 BCEL 473 ASM 62.4 1.1

表 1 的結果表明 ,ASM 的確比其他框架快,而且這一優勢不僅適用於啟動時,還適用於重復使用的時候。

結束語

將 ASM 與其他 classworking 框架進行對比,結果顯示,它比其他框架快若干倍(至少對於這個 相當典型的測試用例是這樣的)。ASM 在結構上更加緊湊,使用的運行時 JAR 大小僅為 33k(對比之下 ,Javassist 的大小為 310K,BCEL 的大小更為驚人,為 504K)。ASM 是否易於使用還很難說,但是它 的接口看起來明顯比 BCEL 的接口更整齊,同時也提供了同樣程度的靈活性(只是缺少一些 BCEL 獨有的 特性,例如成段而非逐行構建代碼的能力)。由於其類似 Java 的源代碼接口,因而 ASM 不像 Javassist 那麼容易使用,但是如果想在字節碼級別上工作,我還是推薦您考慮采用 ASM。

在下 一期文章中,在討論將原來圍繞 BCEL 設計的一個主要 classworking 應用程序轉換成采用 ASM 時,我 還會回到使用 ASM 進行 classworking 的問題上來。下一個月,我將研究如何把 ASM 應用到另一個領域 ,還將考察 J2SE 5.0 添加到 Java 平台上的注釋支持,並展示 ASM 如何處理 J2SE 5.0 注釋,通過一 些很有用的方法來增強這一支持。屆時請回到這裡學習有關這個強大的 classworking 框架的更多內容。

本文配套源碼:http://www.bianceng.net/java/201212/731.htm

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