這是一篇介紹ASM框架比較好的部落格,在這裡推薦下。
原文出自:https://www.ibm.com/developerworks/cn/java/j-lo-asm30/
什麼是 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 等物件,現在要加入一個安全檢查模組, 對已有類的所有操作之前都必須進行一次安全檢查。
圖 1. ASM – AOP
然而 Bank、Customer、Account、Invoice 是代表不同的事務,派生自不同的父類,很難在高層上加入關於 Security Checker 的共有功能。對於沒有多繼承的 Java 來說,更是如此。傳統的解決方案是使用 Decorator 樣式,它可以在一定程度上改善耦合,而功能仍舊是分散的 —— 每個需要 Security Checker 的類都必須要派生一個 Decorator,每個需要 Security Checker 的方法都要被包裝(wrap)。下麵我們以 Account
類為例看一下 Decorator:
首先,我們有一個 SecurityChecker
類,其靜態方法 checkSecurity
執行安全檢查功能:
1
2
3
4
5
6
|
public class SecurityChecker { public static void checkSecurity() { System.out.println("SecurityChecker.checkSecurity ..."); //TODO real security check } } |
另一個是 Account
類:
1
2
3
4
5
6
|
public class Account { public void operation() { System.out.println("operation..."); //TODO real operation } } |
若想對 operation
加入對 SecurityCheck.checkSecurity()
呼叫,標準的 Decorator 需要先定義一個 Account
類的介面:
1
2
3
|
public interface Account { void operation(); } |
然後把原來的 Account
類定義為一個實現類:
1
2
3
4
5
6
|
public class AccountImpl extends Account{ public void operation() { System.out.println("operation..."); //TODO real operation } } |
定義一個 Account
類的 Decorator,並包裝 operation
方法:
1
2
3
4
5
6
7
8
9
10
|
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 更適用於監控和控制虛擬機器的行為。
一種比較理想且流行的方法是使用 java.lang.ref.proxy
。我們仍舊使用上面的例子,給 Account
類加上 checkSecurity 功能 :
首先,Proxy 程式設計是面向介面的。下麵我們會看到,Proxy 並不負責實體化物件,和 Decorator 樣式一樣,要把 Account
定義成一個介面,然後在 AccountImpl
裡實現 Account
介面,接著實現一個 InvocationHandler
Account
方法被呼叫的時候,虛擬機器都會實際呼叫這個 InvocationHandler
的 invoke
方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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
生成代理物件:
1
2
3
4
5
6
7
8
|
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 是面向介面的,所有使用 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。
Java 類檔案概述
所謂 Java 類檔案,就是通常用 javac 編譯器產生的 .class 檔案。這些檔案具有嚴格定義的格式。為了更好的理解 ASM,首先對 Java 類檔案格式作一點簡單的介紹。Java 源檔案經過 javac 編譯器編譯之後,將會生成對應的二進位制檔案(如下圖所示)。每個合法的 Java 類檔案都具備精確的定義,而正是這種精確的定義,才使得 Java 虛擬機器得以正確讀取和解釋所有的 Java 類檔案。
圖 2. ASM – Javac 流程
Java 類檔案是 8 位位元組的二進位制流。資料項按順序儲存在 class 檔案中,相鄰的項之間沒有間隔,這使得 class 檔案變得緊湊,減少儲存空間。在 Java 類檔案中包含了許多大小不同的項,由於每一項的結構都有嚴格規定,這使得 class 檔案能夠從頭到尾被順利地解析。下麵讓我們來看一下 Java 類檔案的內部結構,以便對此有個大致的認識。
例如,一個最簡單的 Hello World 程式:
1
2
3
4
5
|
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello world"); } } |
經過 javac 編譯後,得到的類檔案大致是:
圖 3. ASM – Java 類檔案
從上圖中可以看到,一個 Java 類檔案大致可以歸為 10 個項:
-
Magic:該項存放了一個 Java 類檔案的魔數(magic number)和版本資訊。一個 Java 類檔案的前 4 個位元組被稱為它的魔數。每個正確的 Java 類檔案都是以 0xCAFEBABE 開頭的,這樣保證了 Java 虛擬機器能很輕鬆的分辨出 Java 檔案和非 Java 檔案。
-
Version:該項存放了 Java 類檔案的版本資訊,它對於一個 Java 檔案具有重要的意義。因為 Java 技術一直在發展,所以類檔案的格式也處在不斷變化之中。類檔案的版本資訊讓虛擬機器知道如何去讀取並處理該類檔案。
-
Constant Pool:該項存放了類中各種文字字串、類名、方法名和介面名稱、final 變數以及對外部類的取用資訊等常量。虛擬機器必須為每一個被裝載的類維護一個常量池,常量池中儲存了相應型別所用到的所有型別、欄位和方法的符號取用,因此它在 Java 的動態連結中起到了核心的作用。常量池的大小平均佔到了整個類大小的 60% 左右。
-
Access_flag:該項指明瞭該檔案中定義的是類還是介面(一個 class 檔案中只能有一個類或介面),同時還指名了類或介面的訪問標誌,如 public,private, abstract 等資訊。
-
This Class:指向表示該類全限定名稱的字串常量的指標。
-
Super Class:指向表示父類全限定名稱的字串常量的指標。
-
Interfaces:一個指標陣列,存放了該類或父類實現的所有介面名稱的字串常量的指標。以上三項所指向的常量,特別是前兩項,在我們用 ASM 從已有類派生新類時一般需要修改:將類名稱改為子類名稱;將父類改為派生前的類名稱;如果有必要,增加新的實現介面。
-
Fields:該項對類或介面中宣告的欄位進行了細緻的描述。需要註意的是,fields 串列中僅列出了本類或介面中的欄位,並不包括從超類和父介面繼承而來的欄位。
-
Methods:該項對類或介面中宣告的方法進行了細緻的描述。例如方法的名稱、引數和傳回值型別等。需要註意的是,methods 串列裡僅存放了本類或本介面中的方法,並不包括從超類和父介面繼承而來的方法。使用 ASM 進行 AOP 程式設計,通常是透過調整 Method 中的指令來實現的。
-
Class attributes:該項存放了在該檔案中類或介面所定義的屬性的基本資訊。
事實上,使用 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 事件的過程,對類檔案進行如下操作:
-
刪除類的欄位、方法、指令:只需在職責鏈傳遞過程中中斷委派,不訪問相應的 visit 方法即可,比如刪除方法時只需直接傳回
null
,而不是傳回由visitMethod
方法傳回的MethodVisitor
物件。12345678910111213class 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);
}
}
-
修改類、欄位、方法的名字或修飾符:在職責鏈傳遞過程中替換呼叫引數。
1234567891011class 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 事件的先後呼叫(時間上的先後),最終轉換成位元組碼的位置的調整(空間上的前後),如下例:
1
2
3
4
5
6
|
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 – 時序圖
使用 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
方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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()
的呼叫。
1
2
3
4
5
6
7
8
9
10
|
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
類檔案 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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 檔案,如果我們使用下麵程式碼:
1
2
3
4
5
6
|
public class Main { public static void main(String[] args) { Account account = new Account(); account.operation(); } } |
使用這個 Account,我們會得到下麵的輸出:
1
2
|
SecurityChecker.checkSecurity ... operation... |
也就是說,在 Account
原來的 operation
內容執行之前,進行了 SecurityChecker.checkSecurity()
檢查。
將動態生成類改造成原始類 Account 的子類
上面給出的例子是直接改造 Account
類本身的,從此 Account
類的 operation
方法必須進行 checkSecurity 檢查。但事實上,我們有時仍希望保留原來的 Account
類,因此把生成類定義為原始類的子類是更符合 AOP 原則的做法。下麵介紹如何將改造後的類定義為 Account
的子類 Account$EnhancedByASM
。其中主要有兩項工作 :
-
改變 Class Description, 將其命名為
Account$EnhancedByASM
,將其父類指定為Account
。 -
改變建構式,將其中對父類建構式的呼叫轉換為對
Account
建構式的呼叫。
在 AddSecurityCheckClassAdapter
類中,將重寫 visit
方法:
1
2
3
4
5
6
7
8
|
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
方法,增加對建構式的處理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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
的建構式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
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 的“無損註入”。
小結
最後,我們比較一下 ASM 和其他實現 AOP 的底層技術:
表 1. AOP 底層技術比較
AOP 底層技術 | 功能 | 效能 | 面向介面程式設計 | 程式設計難度 |
---|---|---|---|---|
直接改寫 class 檔案 | 完全控制類 | 無明顯效能代價 | 不要求 | 高,要求對 class 檔案結構和 Java 位元組碼有深刻瞭解 |
JDK Instrument | 完全控制類 | 無論是否改寫,每個類裝入時都要執行 hook 程式 | 不要求 | 高,要求對 class 檔案結構和 Java 位元組碼有深刻瞭解 |
JDK Proxy | 只能改寫 method | 反射引入效能代價 | 要求 | 低 |
ASM | 幾乎能完全控制類 | 無明顯效能代價 | 不要求 | 中,能操縱需要改寫部分的 Java 位元組碼 |
END