1. 程式人生 > >Java ASM 技術簡介

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 的“無損注入”。