Java ASM 技術簡介

什麼是ASM

ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行爲。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,能夠改變類行爲,分析類信息,甚至能夠根據用戶要求生成新類。

與 BCEL 和 SERL 不同,ASM 提供了更爲現代的編程模型。對於 ASM 來說,Java class 被描述爲一棵樹;使用 “Visitor” 模式遍歷整個二進制結構;事件驅動的處理方式使得用戶只需要關注於對其編程有意義的部分,而不必瞭解 Java 類文件格式的所有細節:ASM 框架提供了默認的 “response taker”處理這一切。

爲什麼要動態生成Java類

動態生成 Java 類與 AOP 密切相關的。AOP 的初衷在於軟件設計世界中存在這麼一類代碼,零散而又耦合:零散是由於一些公有的功能(諸如著名的 log 例子)分散在所有模塊之中;同時改變 log 功能又會影響到所有的模塊。出現這樣的缺陷,很大程度上是由於傳統的 面向對象編程注重以繼承關係爲代表的“縱向”關係,而對於擁有相同功能或者說方面 (Aspect)的模塊之間的“橫向”關係不能很好地表達。例如,目前有一個既有的銀行管理系統,包括 Bank、Customer、Account、Invoice 等對象,現在要加入一個安全檢查模塊, 對已有類的所有操作之前都必須進行一次安全檢查。

然而 Bank、Customer、Account、Invoice 是代表不同的事務,派生自不同的父類,很難在高層上加入關於 Security Checker 的共有功能。對於沒有多繼承的 Java 來說,更是如此。傳統的解決方案是使用 Decorator 模式,它可以在一定程度上改善耦合,而功能仍舊是分散的 —— 每個需要 Security Checker 的類都必須要派生一個 Decorator,每個需要 Security Checker 的方法都要被包裝(wrap)。下面我們以 Account類爲例看一下 Decorator:

首先,我們有一個 SecurityChecker類,其靜態方法 checkSecurity執行安全檢查功能:

public class SecurityChecker { 
    public static void checkSecurity() { 
        System.out.println("SecurityChecker.checkSecurity ..."); 
        //TODO real security check 
    }  
}

另一個是 Account類:

public class Account { 
    public void operation() { 
        System.out.println("operation..."); 
        //TODO real operation 
    } 
}

若想對 operation加入對 SecurityCheck.checkSecurity()調用,標準的 Decorator 需要先定義一個 Account類的接口:

public interface Account { 
    void operation(); 
}

然後把原來的 Account類定義爲一個實現類:

public class AccountImpl extends Account{ 
    public void operation() { 
        System.out.println("operation..."); 
        //TODO real operation 
    } 
}

定義一個 Account類的 Decorator,幷包裝 operation方法:

public class AccountWithSecurityCheck implements Account {     
    private  Account account; 
    public AccountWithSecurityCheck (Account account) { 
        this.account = account; 
    } 
    public void operation() { 
        SecurityChecker.checkSecurity(); 
        account.operation(); 
    } 
}

在這個簡單的例子裏,改造一個類的一個方法還好,如果是變動整個模塊,Decorator 很快就會演化成另一個噩夢。動態改變 Java 類就是要解決 AOP 的問題,提供一種得到系統支持的可編程的方法,自動化地生成或者增強 Java 代碼。這種技術已經廣泛應用於最新的 Java 框架內,如 Hibernate,Spring 等。

爲什麼選擇ASM

最直接的改造 Java 類的方法莫過於直接改寫 class 文件。Java 規範詳細說明了 class 文件的格式,直接編輯字節碼確實可以改變 Java 類的行爲。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 文件動手術。是的,這是最直接的方法,但是要求使用者對 Java class 文件的格式了熟於心:小心地推算出想改造的函數相對文件首部的偏移量,同時重新計算 class 文件的校驗碼以通過 Java 虛擬機的安全機制。

Java 5 中提供的 Instrument 包也可以提供類似的功能:啓動時往 Java 虛擬機中掛上一個用戶定義的 hook 程序,可以在裝入特定類的時候改變特定類的字節碼,從而改變該類的行爲。但是其缺點也是明顯的:
- Instrument 包是在整個虛擬機上掛了一個鉤子程序,每次裝入一個新類的時候,都必須執行一遍這段程序,即使這個類不需要改變。
- 直接改變字節碼事實上類似於直接改寫 class 文件,無論是調用 ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer),還是 Instrument.redefineClasses(ClassDefinition[] definitions),都必須提供新 Java 類的字節碼。也就是說,同直接改寫 class 文件一樣,使用 Instrument 也必須瞭解想改造的方法相對類首部的偏移量,才能在適當的位置上插入新的代碼。

儘管 Instrument 可以改造類,但事實上,Instrument 更適用於監控和控制虛擬機的行爲。

首先,Proxy 編程是面向接口的。下面我們會看到,Proxy 並不負責實例化對象,和 Decorator 模式一樣,要把 Account定義成一個接口,然後在 AccountImpl裏實現 Account接口,接着實現一個 InvocationHandlerAccount方法被調用的時候,虛擬機都會實際調用這個 InvocationHandler的 invoke方法:

```
最後,在應用程序中指定 InvocationHandler生成代理對象:
```java




<div class="se-preview-section-delimiter"></div>

其不足之處在於:
- Proxy 是面向接口的,所有使用 Proxy 的對象都必須定義一個接口,而且用這些對象的代碼也必須是對接口編程的:Proxy 生成的對象是接口一致的而不是對象一致的:例子中 Proxy.newProxyInstance生成的是實現 Account接口的對象而不是 AccountImpl的子類。這對於軟件架構設計,尤其對於既有軟件系統是有一定掣肘的。
- Proxy 畢竟是通過反射實現的,必須在效率上付出代價:有實驗數據表明,調用反射比一般的函數開銷至少要大 10 倍。而且,從程序實現上可以看出,對 proxy class 的所有方法調用都要通過使用反射的 invoke 方法。因此,對於性能關鍵的應用,使用 proxy class 是需要精心考慮的,以避免反射成爲整個應用的瓶頸。

ASM 能夠通過改造既有類,直接生成需要的代碼。增強的代碼是硬編碼在新生成的類文件內部的,沒有反射帶來性能上的付出。同時,ASM 與 Proxy 編程不同,不需要爲增強代碼而新定義一個接口,生成的代碼可以覆蓋原來的類,或者是原始類的子類。它是一個普通的 Java 類而不是 proxy 類,甚至可以在應用程序的類框架中擁有自己的位置,派生自己的子類。

相比於其他流行的 Java 字節碼操縱工具,ASM 更小更快。ASM 具有類似於 BCEL 或者 SERP 的功能,而只有 33k 大小,而後者分別有 350k 和 150k。同時,同樣類轉換的負載,如果 ASM 是 60% 的話,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。

ASM 已經被廣泛應用於一系列 Java 項目:AspectWerkz、AspectJ、BEA WebLogic、IBM AUS、OracleBerkleyDB、Oracle TopLink、Terracotta、RIFE、EclipseME、Proactive、Speedo、Fractal、EasyBeans、BeanShell、Groovy、Jamaica、CGLIB、dynaop、Cobertura、JDBCPersistence、JiP、SonarJ、Substance L&F、Retrotranslator 等。Hibernate 和 Spring 也通過 cglib,另一個更高層一些的自動代碼生成工具使用了 ASM。

使用 ASM 動態生成類,不需要像早年的 class hacker 一樣,熟知 class 文件的每一段,以及它們的功能、長度、偏移量以及編碼方式。ASM 會給我們照顧好這一切的,我們只要告訴 ASM 要改動什麼就可以了 —— 當然,我們首先得知道要改什麼:對類文件格式瞭解的越多,我們就能更好地使用 ASM 這個利器。

ASM 3.0 編程框架

ASM 通過樹這種數據結構來表示複雜的字節碼結構,並利用 Push 模型來對樹進行遍歷,在遍歷過程中對字節碼進行修改。所謂的 Push 模型類似於簡單的 Visitor 設計模式,因爲需要處理字節碼結構是固定的,所以不需要專門抽象出一種 Vistable 接口,而只需要提供 Visitor 接口。所謂 Visitor 模式和 Iterator 模式有點類似,它們都被用來遍歷一些複雜的數據結構。Visitor 相當於用戶派出的代表,深入到算法內部,由算法安排訪問行程。Visitor 代表可以更換,但對算法流程無法干涉,因此是被動的,這也是它和 Iterator 模式由用戶主動調遣算法方式的最大的區別。

在 ASM 中,提供了一個 ClassReader類,這個類可以直接由字節數組或由 class 文件間接的獲得字節碼數據,它能正確的分析字節碼,構建出抽象的樹在內存中表示字節碼。它會調用 accept方法,這個方法接受一個實現了 ClassVisitor接口的對象實例作爲參數,然後依次調用 ClassVisitor接口的各個方法。字節碼空間上的偏移被轉換成 visit 事件時間上調用的先後,所謂 visit 事件是指對各種不同 visit 函數的調用,ClassReader知道如何調用各種 visit 函數。在這個過程中用戶無法對操作進行干涉,所以遍歷的算法是確定的,用戶可以做的是提供不同的 Visitor 來對字節碼樹進行不同的修改。ClassVisitor會產生一些子過程,比如 visitMethod會返回一個實現 MethordVisitor接口的實例,visitField會返回一個實現 FieldVisitor接口的實例,完成子過程後控制返回到父過程,繼續訪問下一節點。因此對於 ClassReader來說,其內部順序訪問是有一定要求的。實際上用戶還可以不通過 ClassReader類,自行手工控制這個流程,只要按照一定的順序,各個 visit 事件被先後正確的調用,最後就能生成可以被正確加載的字節碼。當然獲得更大靈活性的同時也加大了調整字節碼的複雜度。

各個 ClassVisitor通過職責鏈 (Chain-of-responsibility) 模式,可以非常簡單的封裝對字節碼的各種修改,而無須關注字節碼的字節偏移,因爲這些實現細節對於用戶都被隱藏了,用戶要做的只是覆寫相應的 visit 函數。

ClassAdaptor類實現了 ClassVisitor接口所定義的所有函數,當新建一個 ClassAdaptor對象的時候,需要傳入一個實現了 ClassVisitor接口的對象,作爲職責鏈中的下一個訪問者 (Visitor),這些函數的默認實現就是簡單的把調用委派給這個對象,然後依次傳遞下去形成職責鏈。當用戶需要對字節碼進行調整時,只需從 ClassAdaptor類派生出一個子類,覆寫需要修改的方法,完成相應功能後再把調用傳遞下去。這樣,用戶無需考慮字節偏移,就可以很方便的控制字節碼。

每個 ClassAdaptor類的派生類可以僅封裝單一功能,比如刪除某函數、修改字段可見性等等,然後再加入到職責鏈中,這樣耦合更小,重用的概率也更大,但代價是產生很多小對象,而且職責鏈的層次太長的話也會加大系統調用的開銷,用戶需要在低耦合和高效率之間作出權衡。用戶可以通過控制職責鏈中 visit 事件的過程,對類文件進行如下操作:
1. 刪除類的字段、方法、指令:只需在職責鏈傳遞過程中中斷委派,不訪問相應的 visit 方法即可,比如刪除方法時只需直接返回 null,而不是返回由 visitMethod方法返回的 MethodVisitor對象。

class DelLoginClassAdapter extends ClassAdapter { 
    public DelLoginClassAdapter(ClassVisitor cv) { 
        super(cv); 
    } 

    public MethodVisitor visitMethod(final int access, final String name, 
        final String desc, final String signature, final String[] exceptions) { 
        if (name.equals("login")) { 
            return null; 
        } 
        return cv.visitMethod(access, name, desc, signature, exceptions); 
    } 
}




<div class="se-preview-section-delimiter"></div>
  1. 修改類、字段、方法的名字或修飾符:在職責鏈傳遞過程中替換調用參數。
class AccessClassAdapter extends ClassAdapter { 
    public AccessClassAdapter(ClassVisitor cv) { 
        super(cv); 
    } 

    public FieldVisitor visitField(final int access, final String name, 
       final String desc, final String signature, final Object value) { 
       int privateAccess = Opcodes.ACC_PRIVATE; 
       return cv.visitField(privateAccess, name, desc, signature, value); 
   } 
}




<div class="se-preview-section-delimiter"></div>
  1. 增加新的類、方法、字段
    ASM 的最終的目的是生成可以被正常裝載的 class 文件,因此其框架結構爲客戶提供了一個生成字節碼的工具類 —— ClassWriter。它實現了 ClassVisitor接口,而且含有一個 toByteArray()函數,返回生成的字節碼的字節流,將字節流寫回文件即可生產調整後的 class 文件。一般它都作爲職責鏈的終點,把所有 visit 事件的先後調用(時間上的先後),最終轉換成字節碼的位置的調整(空間上的前後),如下例:
ClassWriter  classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
ClassAdaptor delLoginClassAdaptor = new DelLoginClassAdapter(classWriter); 
ClassAdaptor accessClassAdaptor = new AccessClassAdaptor(delLoginClassAdaptor); 

ClassReader classReader = new ClassReader(strFileName); 
classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);




<div class="se-preview-section-delimiter"></div>

綜上所述,ASM 的時序圖如下:
image

使用 ASM3.0 進行 AOP 編程

我們還是用上面的例子,給 Account類加上 security check 的功能。與 proxy 編程不同,ASM 不需要將 Account聲明成接口,Account可以仍舊是一個實現類。ASM 將直接在 Account類上動手術,給 Account類的 operation方法首部加上對 SecurityChecker.checkSecurity的調用。

首先,我們將從 ClassAdapter繼承一個類。ClassAdapter是 ASM 框架提供的一個默認類,負責溝通 ClassReader和 ClassWriter。如果想要改變 ClassReader處讀入的類,然後從 ClassWriter處輸出,可以重寫相應的 ClassAdapter函數。這裏,爲了改變 Account類的 operation 方法,我們將重寫 visitMethdod方法。

class AddSecurityCheckClassAdapter extends ClassAdapter {

    public AddSecurityCheckClassAdapter(ClassVisitor cv) {
        //Responsechain 的下一個 ClassVisitor,這裏我們將傳入 ClassWriter,
        // 負責改寫後代碼的輸出
        super(cv); 
    } 

    // 重寫 visitMethod,訪問到 "operation" 方法時,
    // 給出自定義 MethodVisitor,實際改寫方法內容
    public MethodVisitor visitMethod(final int access, final String name, 
        final String desc, final String signature, final String[] exceptions) { 
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
        MethodVisitor wrappedMv = mv; 
        if (mv != null) { 
            // 對於 "operation" 方法
            if (name.equals("operation")) { 
                // 使用自定義 MethodVisitor,實際改寫方法內容
                wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
            } 
        } 
        return wrappedMv; 
    } 
}




<div class="se-preview-section-delimiter"></div>

下一步就是定義一個繼承自 MethodAdapter的 AddSecurityCheckMethodAdapter,在“operation”方法首部插入對 SecurityChecker.checkSecurity()的調用。


class AddSecurityCheckMethodAdapter extends MethodAdapter { 
    public AddSecurityCheckMethodAdapter(MethodVisitor mv) { 
        super(mv); 
    } 

    public void visitCode() { 
        visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", 
           "checkSecurity", "()V"); 
    } 
}




<div class="se-preview-section-delimiter"></div>

其中,ClassReader讀到每個方法的首部時調用 visitCode(),在這個重寫方法裏,我們用 visitMethodInsn(Opcodes.INVOKESTATIC, “SecurityChecker”,”checkSecurity”, “()V”);插入了安全檢查功能。

最後,我們將集成上面定義的 ClassAdapter,ClassReader和 ClassWriter產生修改後的 Account類文件 :

import java.io.File; 
import java.io.FileOutputStream; 
import org.objectweb.asm.*; 

public class Generator{ 
    public static void main() throws Exception { 
        ClassReader cr = new ClassReader("Account"); 
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
        ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw); 
        cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 
        byte[] data = cw.toByteArray(); 
        File file = new File("Account.class"); 
        FileOutputStream fout = new FileOutputStream(file); 
        fout.write(data); 
        fout.close(); 
    } 
}




<div class="se-preview-section-delimiter"></div>

執行完這段程序後,我們會得到一個新的 Account.class 文件,如果我們使用下面代碼:

public class Main { 
    public static void main(String[] args) { 
        Account account = new Account(); 
        account.operation(); 
    } 
}




<div class="se-preview-section-delimiter"></div>

使用這個 Account,我們會得到下面的輸出:

SecurityChecker.checkSecurity ... 
operation...




<div class="se-preview-section-delimiter"></div>

也就是說,在 Account原來的 operation內容執行之前,進行了 SecurityChecker.checkSecurity()檢查。

將動態生成類改造成原始類 Account 的子類
上面給出的例子是直接改造 Account類本身的,從此 Account類的 operation方法必須進行 checkSecurity 檢查。但事實上,我們有時仍希望保留原來的 Account類,因此把生成類定義爲原始類的子類是更符合 AOP 原則的做法。下面介紹如何將改造後的類定義爲 Account的子類 Account$EnhancedByASM。其中主要有兩項工作 :

改變 Class Description, 將其命名爲 Account$EnhancedByASM,將其父類指定爲 Account。
改變構造函數,將其中對父類構造函數的調用轉換爲對 Account構造函數的調用。
在 AddSecurityCheckClassAdapter類中,將重寫 visit方法:

public void visit(final int version, final int access, final String name, 
        final String signature, final String superName, 
        final String[] interfaces) { 
    String enhancedName = name + "$EnhancedByASM";  // 改變類命名
    enhancedSuperName = name; // 改變父類,這裏是”Account”
    super.visit(version, access, enhancedName, signature, 
    enhancedSuperName, interfaces); 
}




<div class="se-preview-section-delimiter"></div>

改進 visitMethod方法,增加對構造函數的處理:

public MethodVisitor visitMethod(final int access, final String name, 
    final String desc, final String signature, final String[] exceptions) { 
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); 
    MethodVisitor wrappedMv = mv; 
    if (mv != null) { 
        if (name.equals("operation")) { 
            wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
        } else if (name.equals("<init>")) { 
            wrappedMv = new ChangeToChildConstructorMethodAdapter(mv, 
                enhancedSuperName); 
        } 
    } 
    return wrappedMv; 
}




<div class="se-preview-section-delimiter"></div>

這裏 ChangeToChildConstructorMethodAdapter將負責把 Account的構造函數改造成其子類 Account$EnhancedByASM的構造函數:

class ChangeToChildConstructorMethodAdapter extends MethodAdapter { 
    private String superClassName; 

    public ChangeToChildConstructorMethodAdapter(MethodVisitor mv, 
        String superClassName) { 
        super(mv); 
        this.superClassName = superClassName; 
    } 

    public void visitMethodInsn(int opcode, String owner, String name, 
        String desc) { 
        // 調用父類的構造函數時
        if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { 
            owner = superClassName; 
        } 
        super.visitMethodInsn(opcode, owner, name, desc);// 改寫父類爲 superClassName 
    } 
}




<div class="se-preview-section-delimiter"></div>

最後演示一下如何在運行時產生並裝入產生的 Account$EnhancedByASM。 我們定義一個 Util 類,作爲一個類工廠負責產生有安全檢查的 Account類:

public class SecureAccountGenerator { 

    private static AccountGeneratorClassLoader classLoader = 
        new AccountGeneratorClassLoade(); 

    private static Class secureAccountClass; 

    public Account generateSecureAccount() throws ClassFormatError, 
        InstantiationException, IllegalAccessException { 
        if (null == secureAccountClass) {            
            ClassReader cr = new ClassReader("Account"); 
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
            ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
            cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 
            byte[] data = cw.toByteArray(); 
            secureAccountClass = classLoader.defineClassFromClassFile( 
               "Account$EnhancedByASM",data); 
        } 
        return (Account) secureAccountClass.newInstance(); 
    } 

    private static class AccountGeneratorClassLoader extends ClassLoader {
        public Class defineClassFromClassFile(String className, 
            byte[] classFile) throws ClassFormatError { 
            return defineClass("Account$EnhancedByASM", classFile, 0, 
            classFile.length());
        } 
    } 
}

靜態方法 SecureAccountGenerator.generateSecureAccount()在運行時動態生成一個加上了安全檢查的 Account子類。著名的 Hibernate 和 Spring 框架,就是使用這種技術實現了 AOP 的“無損注入”。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章