ASM 是一個 Java 字節碼操控框架。它能被用來動態生成類或者加強既有類的功能。ASM 能夠直接產生二進制 class 文件,也能夠在類被加載入 Java 虛擬機以前動態改變類行爲。Java class 被存儲在嚴格格式定義的 .class 文件裏,這些類文件擁有足夠的元數據來解析類中的全部元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM 從類文件中讀入信息後,可以改變類行爲,分析類信息,甚至可以根據用戶要求生成新類。java
與 BCEL 和 SERL 不一樣,ASM 提供了更爲現代的編程模型。對於 ASM 來講,Java class 被描述爲一棵樹;使用 「Visitor」 模式遍歷整個二進制結構;事件驅動的處理方式使得用戶只須要關注於對其編程有意義的部分,而沒必要了解 Java 類文件格式的全部細節:ASM 框架提供了默認的 「response taker」處理這一切。web
動態生成 Java 類與 AOP 密切相關的。AOP 的初衷在於軟件設計世界中存在這麼一類代碼,零散而又耦合:零散是因爲一些公有的功能(諸如著名的 log 例子)分散在全部模塊之中;同時改變 log 功能又會影響到全部的模塊。出現這樣的缺陷,很大程度上是因爲傳統的 面向對象編程注重以繼承關係爲表明的「縱向」關係,而對於擁有相同功能或者說方面 (Aspect)的模塊之間的「橫向」關係不能很好地表達。例如,目前有一個既有的銀行管理系統,包括 Bank、Customer、Account、Invoice 等對象,如今要加入一個安全檢查模塊, 對已有類的全部操做以前都必須進行一次安全檢查。算法
圖 1. ASM – AOP編程
然而 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
方法:app
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 等。
最直接的改造 Java 類的方法莫過於直接改寫 class 文件。Java 規範詳細說明了 class 文件的格式,直接編輯字節碼確實能夠改變 Java 類的行爲。直到今天,還有一些 Java 高手們使用最原始的工具,如 UltraEdit 這樣的編輯器對 class 文件動手術。是的,這是最直接的方法,可是要求使用者對 Java class 文件的格式了熟於心:當心地推算出想改造的函數相對文件首部的偏移量,同時從新計算 class 文件的校驗碼以經過 Java 虛擬機的安全機制。
Java 5 中提供的 Instrument 包也能夠提供相似的功能:啓動時往 Java 虛擬機中掛上一個用戶定義的 hook 程序,能夠在裝入特定類的時候改變特定類的字節碼,從而改變該類的行爲。可是其缺點也是明顯的:
ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer)
,仍是 Instrument.redefineClasses(ClassDefinition[] definitions)
,都必須提供新 Java 類的字節碼。也就是說,同直接改寫 class 文件同樣,使用 Instrument 也必須瞭解想改造的方法相對類首部的偏移量,才能在適當的位置上插入新的代碼。儘管 Instrument 能夠改造類,但事實上,Instrument 更適用於監控和控制虛擬機的行爲。
一種比較理想且流行的方法是使用 java.lang.ref.proxy
。咱們仍舊使用上面的例子,給 Account
類加上 checkSecurity 功能 :
首先,Proxy 編程是面向接口的。下面咱們會看到,Proxy 並不負責實例化對象,和 Decorator 模式同樣,要把 Account
定義成一個接口,而後在 AccountImpl
裏實現 Account
接口,接着實現一個 InvocationHandler
Account
方法被調用的時候,虛擬機都會實際調用這個InvocationHandler
的 invoke
方法:
class SecurityProxyInvocationHandler implements InvocationHandler { private Object proxyedObject; public SecurityProxyInvocationHandler(Object o) { proxyedObject = o; } public Object invoke(Object object, Method method, Object[] arguments) throws Throwable { if (object instanceof Account && method.getName().equals("opertaion")) { SecurityChecker.checkSecurity(); } return method.invoke(proxyedObject, arguments); } }
最後,在應用程序中指定 InvocationHandler
生成代理對象:
public static void main(String[] args) { Account account = (Account) Proxy.newProxyInstance( Account.class.getClassLoader(), new Class[] { Account.class }, new SecurityProxyInvocationHandler(new AccountImpl()) ); account.function(); }
其不足之處在於:
Proxy.newProxyInstance
生成的是實現 Account
接口的對象而不是 AccountImpl
的子類。這對於軟件架構設計,尤爲對於既有軟件系統是有必定掣肘的。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。
所謂 Java 類文件,就是一般用 javac 編譯器產生的 .class 文件。這些文件具備嚴格定義的格式。爲了更好的理解 ASM,首先對 Java 類文件格式做一點簡單的介紹。Java 源文件通過 javac 編譯器編譯以後,將會生成對應的二進制文件(以下圖所示)。每一個合法的 Java 類文件都具有精確的定義,而正是這種精確的定義,才使得 Java 虛擬機得以正確讀取和解釋全部的 Java 類文件。
圖 2. ASM – Javac 流程
Java 類文件是 8 位字節的二進制流。數據項按順序存儲在 class 文件中,相鄰的項之間沒有間隔,這使得 class 文件變得緊湊,減小存儲空間。在 Java 類文件中包含了許多大小不一樣的項,因爲每一項的結構都有嚴格規定,這使得 class 文件可以從頭至尾被順利地解析。下面讓咱們來看一下 Java 類文件的內部結構,以便對此有個大體的認識。
例如,一個最簡單的 Hello World 程序:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello world"); } }
通過 javac 編譯後,獲得的類文件大體是:
圖 3. ASM – Java 類文件
從上圖中能夠看到,一個 Java 類文件大體能夠歸爲 10 個項:
事實上,使用 ASM 動態生成類,不須要像早年的 class hacker 同樣,熟知 class 文件的每一段,以及它們的功能、長度、偏移量以及編碼方式。ASM 會給咱們照顧好這一切的,咱們只要告訴 ASM 要改動什麼就能夠了 —— 固然,咱們首先得知道要改什麼:對類文件格式瞭解的越多,咱們就能更好地使用 ASM 這個利器。
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 事件的過程,對類文件進行以下操做:
刪除類的字段、方法、指令:只需在職責鏈傳遞過程當中中斷委派,不訪問相應的 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); } }
修改類、字段、方法的名字或修飾符:在職責鏈傳遞過程當中替換調用參數。
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); } }
增長新的類、方法、字段
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);
綜上所述,ASM 的時序圖以下:
圖 4. ASM – 時序圖
咱們仍是用上面的例子,給 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; } }
下一步就是定義一個繼承自 MethodAdapter
的 AddSecurityCheckMethodAdapter
,在「operation
」方法首部插入對SecurityChecker.checkSecurity()
的調用。
class AddSecurityCheckMethodAdapter extends MethodAdapter { public AddSecurityCheckMethodAdapter(MethodVisitor mv) { super(mv); } public void visitCode() { visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker", "checkSecurity", "()V"); } }
其中,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(); } }
執行完這段程序後,咱們會獲得一個新的 Account.class 文件,若是咱們使用下面代碼:
public class Main { public static void main(String[] args) { Account account = new Account(); account.operation(); } }
使用這個 Account,咱們會獲得下面的輸出:
SecurityChecker.checkSecurity ... operation...
也就是說,在 Account
原來的 operation
內容執行以前,進行了 SecurityChecker.checkSecurity()
檢查。
上面給出的例子是直接改造 Account
類自己的,今後 Account
類的 operation
方法必須進行 checkSecurity 檢查。但事實上,咱們有時仍但願保留原來的 Account
類,所以把生成類定義爲原始類的子類是更符合 AOP 原則的作法。下面介紹如何將改造後的類定義爲 Account
的子類Account$EnhancedByASM
。其中主要有兩項工做 :
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); }
改進 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; }
這裏 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 } }
最後演示一下如何在運行時產生並裝入產生的 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 的「無損注入」。