使用 ASM 實現 Java 語言的「多重繼承」

http://www.ibm.com/developerworks/cn/java/j-lo-asm/java

問題的提出apache

在大部分狀況下,須要多重繼承每每意味着糟糕的設計。但在處理一些遺留項目的時候,多重繼承多是咱們能作出的選擇中代價最小的。因爲 Java 語言自己不支持多重繼承,這經常會給咱們帶來麻煩,最後的結果可能就是大量的重複代碼。本文試圖使用 ASM 框架來解決這一問題。在擴展類的功能的同時,不產生任何重複代碼。編程

考慮以下的實際狀況:有一組類,名爲 SubClass一、SubClass二、SubClass3 和 SubClass4,它們共同繼承了同一個父類 SuperClass。如今,咱們須要這組類中的一部分,例如 SubClass1 和 SubClass2,這兩個類還要實現另外兩個接口,它們分別爲:IFibonacciComputer 和 ITimeRetriever。然而,這兩個接口已經有了各自的實現類 FibonacciComputer 和 TimeRetriever。而且這兩個類的實現邏輯就是咱們想要的,咱們不想作任何改動,只但願在 SubClass1 和 SubClass2 兩個類中包含這些實現邏輯。數組

它們的結構如圖 1 所示:框架


圖 1. 結構類圖
圖 1. 結構類圖 eclipse

因爲 SubClass1,SubClass2 已經繼承了 SuperClass,因此咱們沒法讓它們再繼承 FibonacciComputer 或 TimeRetriever。ide

因此,想要它們再實現 IFibonacciComputer 和 ITimeRetriever 這兩個接口,必然會產生重複代碼。工具

下面,咱們就使用 ASM 來解決這個問題。性能

 

Java class 文件格式以及類加載器介紹

在後面的內容中,須要對 Java class 文件格式以及類加載器的知識有必定的瞭解,因此這裏先對這些內容作一個簡單介紹:

class 文件格式

Java class 文件的結構如圖 2 所示(圖中「*」表示出現 0 次或任意屢次):


圖 2.Java class 文件結構
圖 2.Java class 文件結構 

詳細說明以下:

  • Magic Number: 每一個 class 文件的前 4 個字節被稱爲「魔數」,它的內容爲:0xCAFEBABE。魔數的做用在於能夠輕鬆地分辨出一個文件是否是 class 文件。
  • Version: 該項指明該 class 文件的版本號。
  • Constant Pool: 常量池是 class 文件中結構最爲複雜,也最爲重要的部分。常量池包含了與文件中類和接口相關的常量。常量池中存儲了諸如文字字符串,final 變量值。Java 虛擬機把常量池組織爲入口列表的形式。常量池中許多入口都指向其餘的常量入口,並且 class 文件中緊隨着常量池的許多條目也都會指向常量池的入口。除了字面常量以外,常量池還能夠容納如下幾種符號引用:類和接口的全限定名,字段的名稱和描述符和方法的名稱和描述符等。
  • Modifiers: 該項指明該文件中定義的是類仍是接口,以及聲明中用了哪一種修飾符,類或接口是私有的,仍是公共的,類的類型是不是 final 的,等等。
  • This class: 該項是對常量池的索引。在這個位置,Java 虛擬機可以找到一個容納了類或接口全限定名的入口。這裏須要注意的是:在 class 文件中,全部類的全限定名都是之內部名稱形式表示的。內部名稱是將原先類全限定名中的「.」替換爲「/」。例如:java.lang.String 的內部名稱爲 java/lang/String。
  • Super Class: 該項也是對常量池的索引,指明瞭該類超類的內部名稱。
  • Interfaces: 該項指明瞭由該類直接實現或由接口擴展的父接口的信息。

:Modifiers,This Class,Super Class 和 Interfaces 這四項的和就是一個類的聲明部分。

  • Annotation: 該項存儲的是註解相關的內容,註解多是關於類的,方法的以及字段的。
  • Attribute: 該項用來存儲關於類,字段以及方法的附加信息。在 Java 5 引入了註解以後,該部份內容幾乎已經沒有用處。
  • Field: 該項用來存儲類的字段信息。
  • Method: 該項用來存儲類的方法信息。

類裝載器介紹

類裝載器負責查找並裝載類。每一個類在被使用以前,都必須先經過類裝載器裝載到 Java 虛擬機當中。Java 虛擬機有兩種類裝載器 :

  • 啓動類裝載器

    啓動類裝載器是 Java 虛擬機實現的一部分,每一個 Java 虛擬機都必須有一個啓動類裝載器,它知道怎麼裝載受信任的類,好比 Java API 的 class 文件。

  • 用戶自定義裝載器

    用戶自定義裝載器是普通的 Java 對象,它的類必須派生自 java.lang.ClassLoader 類。ClassLoader 類中定義的方法爲程序提供了訪問類裝載機制的接口。

 

ASM 簡介以及編程模型

ASM 簡介

ASM 是一個能夠用來生成\轉換和分析 Java 字節碼的代碼庫。其餘相似的工具還有 cglibserp和 BCEL等。相較於這些工具,ASM 有如下的優勢 :

  • ASM 具備簡單、設計良好的 API,這些 API 易於使用。
  • ASM 有很是良好的開發文檔,以及能夠幫助簡化開發的 Eclipse 插件
  • ASM 支持 Java 6
  • ASM 很小、很快、很健壯
  • ASM 有很大的用戶羣,能夠幫助新手解決開發過程當中遇到的問題
  • ASM 的開源許可可讓你幾乎以任何方式使用它

編程模型

ASM 提供了兩種編程模型:

  • Core API,提供了基於事件形式的編程模型。該模型不須要一次性將整個類的結構讀取到內存中,所以這種方式更快,須要更少的內存。但這種編程方式難度較大。
  • Tree API,提供了基於樹形的編程模型。該模型須要一次性將一個類的完整結構所有讀取到內存當中,因此這種方法須要更多的內存。這種編程方式較簡單。

下文中,咱們將只使用 Core API,所以咱們只介紹與其相關的類。

Core API 中操縱字節碼的功能基於 ClassVisitor 接口。這個接口中的每一個方法對應了 class 文件中的每一項。Class 文件中的簡單項的訪問使用一個單獨的方法,方法參數描述了這個項的內容。而那些具備任意長度和複雜度的項,使用另一類方法,這類方法會返回一個輔助的 Visitor 接口,經過這些輔助接口的對象來完成具體內容的訪問。例如 visitField 方法和 visitMethod 方法,分別返回 FieldVisitor 和 MethodVisitor 接口的對象。

清單 1 爲 ClassVisitor 中的方法列表:


清單 1.ClassVisitor 接口中的方法

				
 public interface ClassVisitor { 
   // 訪問類的聲明部分
    void visit(int version, int access, String name, String 
 signature,String superName, String[] interfaces); 
   // 訪問類的代碼
    void visitSource(String source, String debug); 
   // 訪問類的外部類
    void visitOuterClass(String owner, String name, String desc); 
   // 訪問類的註解
    AnnotationVisitor visitAnnotation(String desc, boolean visible); 
   // 訪問類的屬性
    void visitAttribute(Attribute attr); 
   // 訪問類的內部類
    void visitInnerClass(String name, String outerName, 
 String innerName,int access); 
   // 訪問類的字段
    FieldVisitor visitField(int access, String name, String desc, 
 String signature, Object value); 
   // 訪問類的方法
    MethodVisitor visitMethod(int access, String name, 
 String desc,String signature, String[] exceptions); 
   // 訪問結束
    void visitEnd(); 
 } 

 

ClassVisitor 接口中的方法在被調用的時候是有嚴格順序的,其順序如清單 2 所示(其中「?」表示被調用 0 次或 1 次。「*」表示被調用 0 次或任意屢次):


清單 2.ClassVisitor 中方法調用的順序

				
  visit
  visitSource? 
  visitOuterClass? 
  ( visitAnnotation| visitAttribute)* 
  ( visitInnerClass| visitField| visitMethod)* 
  visitEnd
			

 

這就是說,visit 方法必須最早被調用,而後是最多調用一次 visitSource 方法,而後是最多調用一次 visitOuterClass 方法。而後是 visitAnnotation 和 visitAttribute 方法以任意順序被調用任意屢次。再而後是以任任意順序調用 visitInnerClass ,visitField 或 visitMethod 方法任意屢次。最終,調用一次 visitEnd 方法。

ASM 提供了三個基於 ClassVisitor 接口的類來實現 class 文件的生成和轉換:

  • Cla***eader:Cla***eader 解析一個類的 class 字節碼,該類的 accept 方法接受一個 ClassVisitor 的對象,在 accept 方法中,會按上文描述的順序逐個調用 ClassVisitor 對象的方法。它能夠被看作事件的生產者。
  • ClassAdapter:ClassAdapter 是 ClassVisitor 的實現類。它的構造方法中須要一個 ClassVisitor 對象,並保存爲字段 protected ClassVisitor cv。在它的實現中,每一個方法都是原封不動的直接調用 cv 的對應方法,並傳遞一樣的參數。能夠經過繼承 ClassAdapter 並修改其中的部分方法達到過濾的做用。它能夠看作是事件的過濾器。
  • ClassWriter:ClassWriter 也是 ClassVisitor 的實現類。ClassWriter 能夠用來以二進制的方式建立一個類的字節碼。對於 ClassWriter 的每一個方法的調用會建立類的相應部分。例如:調用 visit 方法就是建立一個類的聲明部分,每調用一次 visitMethod 方法就會在這個類中建立一個新的方法。在調用 visitEnd 方法後即代表該類的建立已經完成。它最終生成一個字節數組,這個字節數組中包含了一個類的 class 文件的完整字節碼內容 。能夠經過 toByteArray 方法獲取生成的字節數組。ClassWriter 能夠看作事件的消費者。

一般狀況下,它們是組合起來使用的。

下面舉一個簡單的例子:假設如今須要對 class 文件的版本號進行修改,將其改成 Java 1.5。操做方法以下:

  1. 首先經過 Cla***eader 讀取這個類;
  2. 而後建立一個 ClassAdapter 的子類 ChangeVersionAdapter。在 ChangeVersionAdapter 中,覆蓋 visit 方法,在調用 ClassVisitor#visit 方法時修改其中關於版本號的參數。該方法的簽名以下:visit(int version, int access, String name, String signature, String superName, String[] interfaces),其中每一個參數的含義以下:
    1. version:class 文件的版本號,這就是咱們須要修改的內容;
    2. access:該類的訪問級別;
    3. name:該類的內部名稱;
    4. signature:該類的簽名,若是該類與泛型無關,這個參數就是 null;
    5. superName:父類的內部名稱;
    6. interfaces:該類實現的接口的內部名稱;

明白這些參數的含義以後,修改就很容易,只須要在調用 cv.visit 的時候,將 version 參數指定爲 Opcodes.V1_5 便可(Opcodes 是 ASM 中的一個類),其餘參數不加修改原樣傳遞。這樣,通過該 ClassAdapter 過濾後的類的版本號就都是 Java 1.5 了。

  1. 在建立 ChangeVersionAdapter 對象時,構造方法傳遞一個 ClassWriter 的對象。該 ClassWriter 會隨着 ChangeVersionAdapter 方法的調用按順序建立出類的每個部分。因爲在 visit 方法中,version 參數已經被過濾爲 Opcodes.V1_5,因此該 ClassWriter 最終產生的 class 字節碼的版本號就是 V1.5 的;
  2. 而後經過 ClassWriter#toByteArray 方法獲取修改後的類的完整的字節碼內容;
  3. 固然,想要使用這個類,還須要經過一個自定義類加載器,將得到到的字節碼加載到虛擬機當中,才能夠建立這個類的實例;

代碼片斷如清單 3 所示:


清單 3. 使用 ASM 的代碼示例

				
…
 // 使用 ChangeVersionAdapter 修改 class 文件的版本
 Cla***eader cr = new Cla***eader(className); 
 ClasssWriter cw = new ClassWriter(0); 
 // ChangeVersionAdapter 類是咱們自定義用來修改 class 文件版本號的類
 ClassAdapter ca = new ChangeVersionAdapter (cw); 
 cr.accept(ca, 0); 
 byte[] b2 = cw.toByteArray(); 
…

 

圖 3 是相應的 Sequence 圖:


圖 3. 修改版本號的 Sequence 圖
圖 3. 修改版本號的 Sequence 圖 

代碼示例

在瞭解了以上的知識以後再回到咱們剛開始提出的問題中,咱們但願 SubClass1 和 SubClass2 在繼承自 SuperClass 的同時還要實現 IFibonacciComputer 以及 ITimeRetriever 兩個接口。

爲了後文描述方便,這裏先肯定三個名詞:

  • 實現類即完成了接口實現的類,這裏爲 FibonacciComputer 以及 TimeRetriever。
  • 待加強類即須要實現功能加強,加入實現邏輯的類,這裏爲 SubClass1 和 SubClass2。
  • 加強類即在待加強類的基礎上,加入了接口實現的類。這個類目前沒有實際的類與之對應。

若是隻能在源代碼級別進行修改,咱們能作的僅僅是將實現類的代碼拷貝進待加強類。(固然,有稍微好一點的作法在每個待加強類中包含一個實現類,以組合的方式實現接口。但這仍然不能避免多個待加強類中的代碼重複。)

在學習了 ASM 以後,咱們能夠直接從字節碼的層次來進行修改。回憶一下上文中的內容:使用 ClassWrite 能夠直接建立類的字節碼,不一樣的方法建立了 class 文件的不一樣部分,尤爲重要的是如下幾個方法:

  • visit 方法建立一個類的聲明部分
  • visitField 方法建立一個類的字段
  • visitMethod 方法建立一個類的方法
  • visitEnd 方法,代表該類已經建立完成

因此,如今咱們能夠直接從字節碼的層次完成這一需求:動態的建立一個新的類(即加強類)繼承自待加強類,同時在該類中,將實現類的實現方法添加進來

完整的實現邏輯以下:

  1. 建立一個 ClassAdapter 的子類 AddImplementClassAdapter,這個類將被用來訪問待加強類。AddImplementClassAdapter 期待一個 ClassWriter 做爲 ClassVisitor。該 ClassWriter 在訪問待加強類的同時逐步完成加強類的建立。
  2. 在 AddImplementClassAdapter 中覆蓋 visitEnd 方法,在調用 ClassVisitor#visitEnd 方法以前,使用該 ClassVisitor 逐個訪問每個實現類。因爲實現類中的內容也須要進行必定的過濾,因此這裏的 ClassVisitor 在訪問實現類的時候也須要通過一個 ClassAdapter 進行過濾。建立另外一個 ClassAdapter 的子類 ImplementClassAdapter 來完成這個過濾。因爲這個 ClassVisitor 是一個 ClassWriter。這樣作的效果就是將實現類的內容添加到加強類中。
  3. 在完成了全部實現類的訪問以後,調用 ClassVisitor#visitEnd 方法代表加強類已經建立完成。
  4. 使用一個 Cla***eader 的對象讀取待加強類。
  5. 建立一個 AddImplementClassAdapter 的實例,同時提供一個 ClassWriter 做爲 ClassVisitor。
  6. 經過 accept 方法將前面建立的 AddImplementClassAdapter 對象傳遞給這個 Cla***eader 對象。
  7. 在訪問完待加強類以後,ClassWriter 即完成了加強類的建立,因此最後經過 toByteArray 方法獲取這個加強類的字節碼。
  8. 最後經過一個自定義類加載器將其加載到虛擬機當中。

下面是代碼示例與講解:

首先須要修改 SubClass1 以及 SubClass2 兩個類,使其聲明實現 IFibonacciComputer 和 ITimeRetriever 這兩個接口。因爲這兩個類並無真正的包含實現接口的代碼,因此它們如今必須標記爲抽象類。修改後的類結構如圖 4 所示:


圖 4. 修改後的類圖
圖 4. 修改後的類圖 

而後建立如下幾個類:

  • AddImplementClassAdapter: 過濾待加強類,並引導 ClassWriter 建立加強類的適配器。
  • ImplementClassAdapter: 實現類的適配器,過濾多餘內容,將實現類中的內容經過 ClassWriter 添加到加強類中。
  • ModifyInitMethodAdapter: 修改待加強類構造方法的適配器。
  • SimpleClassLoader: 自定義類加載器,用來加載動態生成的加強類。
  • EnhanceFactory: 提供對外接口,方便使用。
  • EnhanceException: 對異常的統一包裝,方便異常處理。

下面,咱們來逐一實現這些類:

AddImplementClassAdapter: 首先在構造方法中,咱們須要記錄下待加強類的 Class 對象,加強類的類名,實現類的 Class 對象,以及一個 ClassWriter 對象,該構造方法代碼如清單 4 所示:


清單 4.AddImpelementClassAdapter 構造方法代碼

				
 public AddImplementClassAdapter( ClassWriter writer, 
 String enhancedClassName,Class<?> targetClass, 
 Class<?>... implementClasses) { 
    super(writer); 
    this.classWriter = writer; 
    this.implementClasses = implementClasses; 
    this.originalClassName = targetClass.getName(); 
    this.enhancedClassName = enhancedClassName; 
 } 

 

在 visit 方法中須要完成加強類聲明部分的建立,加強類繼承自待加強類。該方法代碼如清單 6 所示:


清單 5.visit 方法代碼

// 經過 visit 方法完成加強類的聲明部分			
 public void visit(int version, int access, String name, 
 String signature,String superName, String[] interfaces) { 
      cv.visit(version, Opcodes.ACC_PUBLIC, 
     // 將 Java 代碼中類的名稱替換爲虛擬機中使用的形式
      enhancedClassName.replace('.', '/'), 
      signature, name,interfaces); 
 } 

 

visitMethod 方法中須要對構造方法作單獨處理,由於 class 文件中的構造方法與源代碼中的構造方法有三點不同的地方:

  1. 每一個 class 文件中至少有一個構造方法。即使你在類的源代碼中沒有編寫構造方法,編譯器也會爲你生成一個默認構造方法 ;
  2. 在 class 文件中與源代碼中的構造方法名稱不同,class 文件的構造方法名稱都爲「<init>」;
  3. class 文件中每一個構造方法都會最早調用一次父類的構造方法。

鑑於這些緣由,加強類的構造方法須要在待加強類構造方法的基礎上進行修改。修改的內容就是對於父構造方法的調用,由於加強類和待加強類的父類是不同的。

visitMethod 方法中須要判斷若是是構造方法就經過 ModifyInitMethodAdapter 修改構造方法。其餘方法直接返回 null 丟棄(由於加強類已經從待加強類中繼承了這些方法,因此這些方法不須要在加強類中再出現一遍),該方法代碼如清單 7 所示:


清單 6.visitMethod 方法代碼

				
 public MethodVisitor visitMethod(int access, String name, 
 String desc,String signature, String[] exceptions) { 
    if (INTERNAL_INIT_METHOD_NAME.equals(name)) { 
       // 經過 ModifyInitMethodAdapter 修改構造方法
        MethodVisitor mv = classWriter.visitMethod(access, 
        INTERNAL_INIT_METHOD_NAME, desc, signature, exceptions); 
        return new ModifyInitMethodAdapter(mv, originalClassName); 
    } 
    return null; 
 } 

 

最後,在 visitEnd 方法,使用 ImplementClassAdapter 與 ClassWriter 將實現類的內容添加到加強類中,最後再調用 visitEnd 方法代表加強類已經建立完成:


清單 7.visitEnd 方法代碼

				
 public void visitEnd() { 
 for (Class<?> clazz : implementClasses) { 
    try { 
      // 逐個將實現類的內容添加到加強類中。
      Cla***eader reader = new Cla***eader(clazz.getName()); 
      ClassAdapter adapter = 
      new ImplementClassAdapter(classWriter); 
      reader.accept(adapter, 0); 
    } catch (IOException e) { 
        e.printStackTrace(); 
    } 
 } 
 cv.visitEnd(); 
 } 

 

ImplementClassAdapter:該類對實現類進行過濾。

首先在 visit 方法中給於空實現將類的聲明部分過濾掉,代碼如清單 8 所示:


清單 8.visit 方法代碼

				
 public void visit(int version, int access, String name, 
 String signature,String superName, String[] interfaces) { 
    // 空實現,將該部份內容過濾掉
 } 

 

而後在 visitMethod 中,將構造方法過濾掉,對於其餘方法,調用 ClassVisitor#visitMethod 進行訪問。因爲這裏的 ClassVisitor 是一個 ClassWriter,這就至關於在加強類中建立了該方法,代碼如清單 9 所示:


清單 9.visitMethod 方法代碼

				
 public MethodVisitor visitMethod(int access, String name, 
 String desc,String signature, String[] exceptions) { 
    // 過濾掉實現類中的構造方法
    if (AddImplementClassAdapter.INTERNAL_INIT_METHOD_NAME.equals(name)){ 
        return null; 
    } 
    // 其餘方法原樣保留
    return cv.visitMethod(access, name, desc, signature, exceptions); 
 } 

 

ModifyInitMethodAdapter:上文中已經提到,ModifyInitMethodAdapter 是用來對加強類的構造方法進行修改的。MethodAdapter 中的 visitMethodInsn 是對方法調用指令的訪問。該方法的參數含義以下:

  • opcode 爲調用方法的 JVM 指令,
  • owner 爲被調用方法的類名,
  • name 爲方法名,
  • desc 爲方法描述。

因此,咱們須要將對於待加強類父類構造方法的調用改成對於待加強類構造方法的調用(由於加強類的父類就是待加強類),其代碼如清單 10 所示:


清單 10. ModifyInitMethodAdapter 類代碼

				
	  /** 
	     專門用來修改構造方法的方法適配器
	  */ 
 public class ModifyInitMethodAdapter extends MethodAdapter { 
 private String className; 
 public ModifyInitMethodAdapter(MethodVisitor mv, String name) { 
    super(mv); 
    this.className = name; 
 } 
 public void visitMethodInsn(int opcode, String owner, 
 String name,String desc) { 
    // 將 Java 代碼中的類全限定名替換爲虛擬機中使用的形式
    if (name.equals(AddImplementClassAdapter.INTERNAL_INIT_METHOD_NAME)) { 
    mv.visitMethodInsn(opcode, className.replace(".", "/"), 
    name, desc); 
 } 
 } 
 } 

 

SimpleClassLoader:該自定義類裝載器經過提供一個 defineClass 方法來裝載動態生成的加強類。方法的實現是直接調用父類的 defineClass 方法,其代碼如清單 11 所示:


清單 11. SimpleClassLoader 類代碼

				
 public class SimpleClassLoader extends ClassLoader { 
 public Class<?> defineClass(String className, byte[] byteCodes) { 
        // 直接經過父類的 defineClass 方法加載類的結構
    return super.defineClass(className, byteCodes, 
    0, byteCodes.length); 
 } 
 } 

 

EnhanceException:這是一個異常包裝類,其中包含了待加強類和實現類的信息,其邏輯很簡單,代碼如清單 12 所示:


清單 12. EnhanceException 類代碼

				
/** 
 異常類
*/ 
 public class EnhanceException extends Exception { 
 private Class<?> enhanceClass; 
 private Class<?> [] implementClasses; 
 // 異常類構造方法
 public EnhanceException(Exception ex,Class<?> ec,Class<?>... imClazz){ 
    super(ex); 
    this.enhanceClass = ec; 
    this.implementClasses = imClazz; 
 } 
 public Class<?> getEnhanceClass() { 
    return enhanceClass; 
 } 
 public Class<?>[] getImplementClasses() { 
    return implementClasses; 
 } 
 } 

 

EnhanceFactory:最後,經過 EnhanceFactory 提供對外調用接口,調用接口有兩個:

  • public static <T> Class<T> addImplementation(
    Class<T> clazz,Class<?>... implementClasses)
  • public static <T> T newInstance(Class<T> clazz,
    Class<?>... impls)

爲了方便使用,這兩個方法都使用了泛型。它們的參數是同樣的:第一個參數都是待加強類的 Class 對象,後面是任意多個實現類的 Class 對象,返回的類型和待加強類一致,用戶在獲取返回值以後不須要進行任何類型轉換便可使用。

第一個方法建立出加強類的 Class 對象,並經過自定義類加載器加載,其代碼如清單 13 所示:


清單 13. addImplementation 方法代碼

				
/** 
靜態工具方法,在待加強類中加入實現類的內容,返回加強類。
*/ 
 public static <T> Class<T> addImplementation(Class<T> clazz, 
 Class<?>... implementClasses) throws EnhanceException { 
    String enhancedClassName = clazz.getName() + ENHANCED; 
    try { 
       // 嘗試加載加強類
        return  (Class<T>) classLoader.loadClass(enhancedClassName); 
        } 
       // 若是沒有找到加強類,則嘗試直接在內存中構建出加強類的結構
    catch (ClassNotFoundException classNotFoundException) { 
        Cla***eader reader = null; 
        try { 
            reader = new Cla***eader(clazz.getName()); 
        } catch (IOException ioexception) { 
            throw new EnhanceException(ioexception, 
            clazz, implementClasses); 
        } 
        ClassWriter writer = new ClassWriter(0); 
       // 經過 AddImplementClassAdapter 完成實現類內容的織入
        ClassVisitor visitor = new AddImplementClassAdapter( 
        enhancedClassName, clazz, writer, implementClasses); 
        reader.accept(visitor, 0); 
        byte[] byteCodes = writer.toByteArray(); 
        Class<T> result = (Class<T>) classLoader.defineClass( 
        enhancedClassName, byteCodes); 
        return result; 
    } 
 } 

 

第二個方法先調用前一個方法,獲取 加強類的 Class對象,而後使用反射建立實例,其代碼如清單 14 所示:


清單 14. newInstance 方法代碼

				
/** 
經過待加強類和實現類,獲得加強類的實例對象
*/ 
 public static <T> T newInstance(Class<T> clazz, Class<?>... impls) 
 throws EnhanceException { 
 Class<T> c = addImplementation(clazz, impls); 
 if (c == null) { 
    return null; 
 } 
 try { 
       // 經過反射建立實例
    return c.newInstance(); 
 } catch (InstantiationException e) { 
    throw new EnhanceException(e, clazz, impls); 
 } catch (IllegalAccessException e) { 
    throw new EnhanceException(e, clazz, impls); 
 } 
 } 

 

下面是測試代碼,先經過 EnhanceFactory 建立加強類的實例,而後就能夠像普通對象同樣的使用,代碼如清單 15 所示:


清單 15. 使用演示代碼

				
 // 不用 new 關鍵字,而使用 EnhanceFactory.newInstance 建立加強類的實例
 SubClass1 obj1 = EnhanceFactory.newInstance(SubClass1.class, 
 TimeRetriever.class,FibonacciComputer.class); 
 // 調用待加強類中的方法
 obj1.methodInSuperClass(); 
 obj1.methodDefinedInSubClass1(); 
 // 調用實現類中的方法
 System.out.println("The Fibonacci number of 10 is "+obj1.compute(10)); 
 System.out.println("Now is :"+obj1.tellMeTheTime()); 
 System.out.println("--------------------------------------"); 
 // 對於 SubClass2 的加強類實例的建立也是同樣的
 SubClass2 obj2 = EnhanceFactory.newInstance(SubClass2.class, 
 TimeRetriever.class,FibonacciComputer.class); 
 // 調用待加強類中的方法
 obj2.methodInSuperClass(); 
 obj2.methodDefinedInSubClass2(); 
 // 調用實現類中的方法
 System.out.println("The Fibonacci number of 10 is "+obj1.compute(10)); 
 System.out.println("Now is :"+obj1.tellMeTheTime()); 

 

這裏,咱們演示了使用 ASM 建立一個新的類,而且修改該類中的內容的方法。而這一切都是在運行的環境中動態生成的,這一點相較於源代碼級別的實現有如下的好處:

  • 沒有重複代碼 這是咱們的主要目的,因爲加強類是在運行的環境中生成的,而且動態的包含了實現類中的內容,因此,不會產生任何重複代碼。
  • 靈活性 使用 EnhanceFactory# addImplementation 方法,對於接口的實現徹底是在運行時肯定的,所以能夠靈活的選擇。
  • 可複用性 EnhanceFactory# addImplementation 是一個能夠徹底複用的方法,咱們能夠在任何須要的地方使用它。

須要注意的是,這裏咱們並無真正的實現「多重繼承」,因爲 class 文件格式的限制,咱們也不可能真正實現「多重繼承」,咱們只是在一個類中包含了多個實現類的內容而已。可是,若是你使用加強類的實例經過 instanceof 之類的方法來判斷它是不是實現類的實例的時候,你會獲得 false,由於加強類並無真正的繼承自實現類。

另外,爲了讓演示代碼足夠的簡單,對於這個功能的實現還存在一些問題,例如:

  • FibonacciComputer 和 TimeRetriever 這兩個類中,可能會包含一些其餘方法,這些方法並不是是爲了實現接口的方法,而這些方法也會被加強類所包含。
  • 若是多個實現類與待加強類中包含了一樣簽名的方法時,在建立加強類的過程當中會產生異常,由於一個類中不能包含一樣方法簽名的兩個方法。
  • 若是實現類中包含了一些字段,而且實現類的構造方法中初始化了這些字段。但加強類中的這些字段並無被初始化。由於實現類的構造方法被忽略了。
  • 沒法對同一個類作屢次不一樣類型的加強。

不過,在理解了上文中的知識以後,這些問題也都是能夠解決的。

做爲一個能夠操做字節碼的工具而言,ASM 的功能遠不止於此。它還能夠用來實現 AOP,實現性能監測,方法調用統計等功能。經過 Google,能夠很容易的找到這類文章。

示例代碼包含在 ASM_Demo.zip 中,該文件中包含了上文中提到的全部代碼。

該 zip 文件爲 eclipse 項目的歸檔文件。能夠經過 Eclipse 菜單導入至 Eclipse 中,導入方法:File -> Import … -> Existing Projects into Workspace, 而後選擇該 zip 文件便可。

想要編譯該項目,還須要 ASM 框架的 jar 包。請在如下地址下載 ASM 框架:http://forge.ow2.org/projects/asm/

目前該框架正式版的版本號爲:3.3.1。

下載該框架歸檔文件後解壓縮, 並經過 eclipse 將 asm-all-3.3.1.jar(多是其餘版本號)添加到項目編譯的類路徑中便可。

代碼中包含的 Main 類,是一個包含了 main 方法的可執行類,在 eclipse 中運行該類便可看到運行結果。

相關文章
相關標籤/搜索