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. 結構類圖
eclipse
因爲 SubClass1,SubClass2 已經繼承了 SuperClass,因此咱們沒法讓它們再繼承 FibonacciComputer 或 TimeRetriever。ide
因此,想要它們再實現 IFibonacciComputer 和 ITimeRetriever 這兩個接口,必然會產生重複代碼。工具
下面,咱們就使用 ASM 來解決這個問題。性能
回頁首學習
在後面的內容中,須要對 Java class 文件格式以及類加載器的知識有必定的瞭解,因此這裏先對這些內容作一個簡單介紹:
Java class 文件的結構如圖 2 所示(圖中「*」表示出現 0 次或任意屢次):
詳細說明以下:
注:Modifiers,This Class,Super Class 和 Interfaces 這四項的和就是一個類的聲明部分。
類裝載器負責查找並裝載類。每一個類在被使用以前,都必須先經過類裝載器裝載到 Java 虛擬機當中。Java 虛擬機有兩種類裝載器 :
啓動類裝載器是 Java 虛擬機實現的一部分,每一個 Java 虛擬機都必須有一個啓動類裝載器,它知道怎麼裝載受信任的類,好比 Java API 的 class 文件。
用戶自定義裝載器是普通的 Java 對象,它的類必須派生自 java.lang.ClassLoader 類。ClassLoader 類中定義的方法爲程序提供了訪問類裝載機制的接口。
ASM 是一個能夠用來生成\轉換和分析 Java 字節碼的代碼庫。其餘相似的工具還有 cglib、serp和 BCEL等。相較於這些工具,ASM 有如下的優勢 :
ASM 提供了兩種編程模型:
下文中,咱們將只使用 Core API,所以咱們只介紹與其相關的類。
Core API 中操縱字節碼的功能基於 ClassVisitor 接口。這個接口中的每一個方法對應了 class 文件中的每一項。Class 文件中的簡單項的訪問使用一個單獨的方法,方法參數描述了這個項的內容。而那些具備任意長度和複雜度的項,使用另一類方法,這類方法會返回一個輔助的 Visitor 接口,經過這些輔助接口的對象來完成具體內容的訪問。例如 visitField 方法和 visitMethod 方法,分別返回 FieldVisitor 和 MethodVisitor 接口的對象。
清單 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 次或任意屢次):
visit visitSource? visitOuterClass? ( visitAnnotation| visitAttribute)* ( visitInnerClass| visitField| visitMethod)* visitEnd |
這就是說,visit 方法必須最早被調用,而後是最多調用一次 visitSource 方法,而後是最多調用一次 visitOuterClass 方法。而後是 visitAnnotation 和 visitAttribute 方法以任意順序被調用任意屢次。再而後是以任任意順序調用 visitInnerClass ,visitField 或 visitMethod 方法任意屢次。最終,調用一次 visitEnd 方法。
ASM 提供了三個基於 ClassVisitor 接口的類來實現 class 文件的生成和轉換:
一般狀況下,它們是組合起來使用的。
下面舉一個簡單的例子:假設如今須要對 class 文件的版本號進行修改,將其改成 Java 1.5。操做方法以下:
明白這些參數的含義以後,修改就很容易,只須要在調用 cv.visit 的時候,將 version 參數指定爲 Opcodes.V1_5 便可(Opcodes 是 ASM 中的一個類),其餘參數不加修改原樣傳遞。這樣,通過該 ClassAdapter 過濾後的類的版本號就都是 Java 1.5 了。
代碼片斷如清單 3 所示:
… // 使用 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 圖:
在瞭解了以上的知識以後再回到咱們剛開始提出的問題中,咱們但願 SubClass1 和 SubClass2 在繼承自 SuperClass 的同時還要實現 IFibonacciComputer 以及 ITimeRetriever 兩個接口。
爲了後文描述方便,這裏先肯定三個名詞:
若是隻能在源代碼級別進行修改,咱們能作的僅僅是將實現類的代碼拷貝進待加強類。(固然,有稍微好一點的作法在每個待加強類中包含一個實現類,以組合的方式實現接口。但這仍然不能避免多個待加強類中的代碼重複。)
在學習了 ASM 以後,咱們能夠直接從字節碼的層次來進行修改。回憶一下上文中的內容:使用 ClassWrite 能夠直接建立類的字節碼,不一樣的方法建立了 class 文件的不一樣部分,尤爲重要的是如下幾個方法:
因此,如今咱們能夠直接從字節碼的層次完成這一需求:動態的建立一個新的類(即加強類)繼承自待加強類,同時在該類中,將實現類的實現方法添加進來。
完整的實現邏輯以下:
下面是代碼示例與講解:
首先須要修改 SubClass1 以及 SubClass2 兩個類,使其聲明實現 IFibonacciComputer 和 ITimeRetriever 這兩個接口。因爲這兩個類並無真正的包含實現接口的代碼,因此它們如今必須標記爲抽象類。修改後的類結構如圖 4 所示:
而後建立如下幾個類:
下面,咱們來逐一實現這些類:
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 所示:
// 經過 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 文件中的構造方法與源代碼中的構造方法有三點不同的地方:
鑑於這些緣由,加強類的構造方法須要在待加強類構造方法的基礎上進行修改。修改的內容就是對於父構造方法的調用,由於加強類和待加強類的父類是不同的。
visitMethod 方法中須要判斷若是是構造方法就經過 ModifyInitMethodAdapter 修改構造方法。其餘方法直接返回 null 丟棄(由於加強類已經從待加強類中繼承了這些方法,因此這些方法不須要在加強類中再出現一遍),該方法代碼如清單 7 所示:
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 方法代表加強類已經建立完成:
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 所示:
public void visit(int version, int access, String name, String signature,String superName, String[] interfaces) { // 空實現,將該部份內容過濾掉 } |
而後在 visitMethod 中,將構造方法過濾掉,對於其餘方法,調用 ClassVisitor#visitMethod 進行訪問。因爲這裏的 ClassVisitor 是一個 ClassWriter,這就至關於在加強類中建立了該方法,代碼如清單 9 所示:
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 是對方法調用指令的訪問。該方法的參數含義以下:
因此,咱們須要將對於待加強類父類構造方法的調用改成對於待加強類構造方法的調用(由於加強類的父類就是待加強類),其代碼如清單 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 所示:
public class SimpleClassLoader extends ClassLoader { public Class<?> defineClass(String className, byte[] byteCodes) { // 直接經過父類的 defineClass 方法加載類的結構 return super.defineClass(className, byteCodes, 0, byteCodes.length); } } |
EnhanceException:這是一個異常包裝類,其中包含了待加強類和實現類的信息,其邏輯很簡單,代碼如清單 12 所示:
/** 異常類 */ 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 所示:
/** 靜態工具方法,在待加強類中加入實現類的內容,返回加強類。 */ 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 所示:
/** 經過待加強類和實現類,獲得加強類的實例對象 */ 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 所示:
// 不用 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 建立一個新的類,而且修改該類中的內容的方法。而這一切都是在運行的環境中動態生成的,這一點相較於源代碼級別的實現有如下的好處:
須要注意的是,這裏咱們並無真正的實現「多重繼承」,因爲 class 文件格式的限制,咱們也不可能真正實現「多重繼承」,咱們只是在一個類中包含了多個實現類的內容而已。可是,若是你使用加強類的實例經過 instanceof 之類的方法來判斷它是不是實現類的實例的時候,你會獲得 false,由於加強類並無真正的繼承自實現類。
另外,爲了讓演示代碼足夠的簡單,對於這個功能的實現還存在一些問題,例如:
不過,在理解了上文中的知識以後,這些問題也都是能夠解決的。
做爲一個能夠操做字節碼的工具而言,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 中運行該類便可看到運行結果。