ASM字節碼增強技術
ASM是什么?
asm是字節碼增強技術,通過asm可以生成新的class文件,也可以動態的修改即將要裝載入jvm的類信息。
一、什么是ASM
ASM是一個java字節碼操縱框架,它能被用來動態生成類或者增強既有類的功能。ASM 可以直接產生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態改變類行為。Java class 被存儲在嚴格格式定義的 .class文件里,這些類文件擁有足夠的元數據來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節碼(指令)。ASM從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據用戶要求生成新類。asm字節碼增強技術主要是用來反射的時候提升性能的,如果單純用jdk的反射調用,性能是非常低下的,而使用字節碼增強技術后反射調用的時間已經基本可以與直接調用相當了
使用ASM框架需要導入asm的jar包,下載鏈接:asm-3.2.jar。
二、如何使用ASM
ASM框架中的核心類有以下幾個:
① ClassReader:該類用來解析編譯過的class字節碼文件。
② ClassWriter:該類用來重新構建編譯后的類,比如說修改類名、屬性以及方法,甚至可以生成新的類的字節碼文件。
③ ClassAdapter:該類也實現了ClassVisitor接口,它將對它的方法調用委托給另一個ClassVisitor對象。
三、 ASM字節碼處理框架是用Java開發的而且使用基於訪問者模式生成字節碼及驅動類到字節碼的轉換,通俗的講,它就是對class文件的CRUD,經過CRUD后的字節碼可以轉換為類。ASM的解析方式類似於SAX解析XML文件,它綜合運用了訪問者模式、職責鏈模式、橋接模式等多種設計模式,相對於其他類似工具如BCEL、SERP、Javassist、CGLIB,它的最大的優勢就在於其性能更高,其jar包僅30K。Hibernate和Spring都使用了cglib代理,而cglib本身就是使用的ASM,可見ASM在各種開源框架都有廣泛的應用。
ASM是一個強大的框架,利用它我們可以做到:
1、獲得class文件的詳細信息,包括類名、父類名、接口、成員名、方法名、方法參數名、局部變量名、元數據等
2、對class文件進行動態修改,如增加、刪除、修改類方法、在某個方法中添加指令等
CGLIB(動態代理)是對ASM的封裝,簡化了ASM的操作,降低了ASM的使用門檻,
其中,hibernate的懶加載使用到了asm,spring的AOP也使用到了。你建立一個hibernate映射對象並使用懶加載配置的時候,在內存中生成的對象使用的不再是你實現的那個類了,而是hibernate根據字節碼技術已你的類為模板構造的一個新類,證明就是當你獲得那個對象輸出類名是,不是你自己生成的類名了。spring可能是proxy$xxx,hibernate可能是<你的類名>$xxx$xxx之類的名字。
AOP 的利器:ASM 3.0 介紹
隨着 AOP(Aspect Oriented Programming)的發展,代碼動態生成已然成為 Java 世界中不可或缺的一環。本文將介紹一種小巧輕便的 Java 字節碼操控框架 ASM,它能方便地生成和改造 Java 代碼。著名的框架,如 Hibernate 和 Spring 在底層都用到了 ASM。比起傳統的 Java 字節碼操控框架,BCEL 或者 SERP,它具有更符合現代軟件模式的編程模型和更迅捷的性能。
本文主要分為四個部分:首先將 ASM 和其他 Java 類生成方案作對比,然后大致介紹 Java 類文件的組織,最后針對最新的 ASM 3.0,描述其編程框架,並給出一個使用 ASM 進行 AOP 的例子,介紹調整函數內容,生成派生類,以及靜態和動態生成類的方法。
引言
什么是 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
執行安全檢查功能:
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 更適用於監控和控制虛擬機的行為。
一種比較理想且流行的方法是使用 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 是面向接口的,所有使用 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 程序:
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
對象。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 – 時序圖
使用 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; } }
下一步就是定義一個繼承自 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
類本身的,從此 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); }
改進 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 的“無損注入”。
小結
最后,我們比較一下 ASM 和其他實現 AOP 的底層技術:
表 1. AOP 底層技術比較
AOP 底層技術 | 功能 | 性能 | 面向接口編程 | 編程難度 |
---|---|---|---|---|
直接改寫 class 文件 | 完全控制類 | 無明顯性能代價 | 不要求 | 高,要求對 class 文件結構和 Java 字節碼有深刻了解 |
JDK Instrument | 完全控制類 | 無論是否改寫,每個類裝入時都要執行 hook 程序 | 不要求 | 高,要求對 class 文件結構和 Java 字節碼有深刻了解 |
JDK Proxy | 只能改寫 method | 反射引入性能代價 | 要求 | 低 |
ASM | 幾乎能完全控制類 | 無明顯性能代價 | 不要求 | 中,能操縱需要改寫部分的 Java 字節碼 |