[轉(zhuǎn)載]AOP 的利器:ASM 3.0 介紹

引言

什么是 ASM 般妙?

ASM 是一個(gè) Java 字節(jié)碼操控框架晌端。它能被用來動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能。ASM 可以直接產(chǎn)生二進(jìn)制 class 文件,也可以在類被加載入 Java 虛擬機(jī)之前動(dòng)態(tài)改變類行為庄蹋。Java class 被存儲(chǔ)在嚴(yán)格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱迷雪、方法限书、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后章咧,能夠改變類行為倦西,分析類信息,甚至能夠根據(jù)用戶要求生成新類赁严。

與 BCEL 和 SERL 不同扰柠,ASM 提供了更為現(xiàn)代的編程模型领炫。對(duì)于 ASM 來說幽歼,Java class 被描述為一棵樹画畅;使用 “Visitor” 模式遍歷整個(gè)二進(jìn)制結(jié)構(gòu)谣辞;事件驅(qū)動(dòng)的處理方式使得用戶只需要關(guān)注于對(duì)其編程有意義的部分,而不必了解 Java 類文件格式的所有細(xì)節(jié):ASM 框架提供了默認(rèn)的 “response taker”處理這一切劝枣。

為什么要?jiǎng)討B(tài)生成 Java 類汤踏?

動(dòng)態(tài)生成 Java 類與 AOP 密切相關(guān)的。AOP 的初衷在于軟件設(shè)計(jì)世界中存在這么一類代碼舔腾,零散而又耦合:零散是由于一些公有的功能(諸如著名的 log 例子)分散在所有模塊之中溪胶;同時(shí)改變 log 功能又會(huì)影響到所有的模塊。出現(xiàn)這樣的缺陷稳诚,很大程度上是由于傳統(tǒng)的 面向?qū)ο缶幊套⒅匾岳^承關(guān)系為代表的“縱向”關(guān)系哗脖,而對(duì)于擁有相同功能或者說方面 (Aspect)的模塊之間的“橫向”關(guān)系不能很好地表達(dá)。例如扳还,目前有一個(gè)既有的銀行管理系統(tǒng)才避,包括 Bank、Customer普办、Account工扎、Invoice 等對(duì)象徘钥,現(xiàn)在要加入一個(gè)安全檢查模塊衔蹲, 對(duì)已有類的所有操作之前都必須進(jìn)行一次安全檢查。

圖 1. ASM – AOP
圖 1. ASM – AOP

然而 Bank呈础、Customer舆驶、Account、Invoice 是代表不同的事務(wù)而钞,派生自不同的父類沙廉,很難在高層上加入關(guān)于 Security Checker 的共有功能。對(duì)于沒有多繼承的 Java 來說臼节,更是如此撬陵。傳統(tǒng)的解決方案是使用 Decorator 模式,它可以在一定程度上改善耦合网缝,而功能仍舊是分散的 —— 每個(gè)需要 Security Checker 的類都必須要派生一個(gè) Decorator巨税,每個(gè)需要 Security Checker 的方法都要被包裝(wrap)。下面我們以 Account類為例看一下 Decorator:

首先粉臊,我們有一個(gè) SecurityChecker類草添,其靜態(tài)方法 checkSecurity執(zhí)行安全檢查功能:

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

另一個(gè)是 Account類:

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

若想對(duì) operation加入對(duì) SecurityCheck.checkSecurity()調(diào)用,標(biāo)準(zhǔn)的 Decorator 需要先定義一個(gè) Account類的接口:

<pre class="displaycode" name="code" style="box-sizing: border-box; outline: 0px; padding: 8px; margin: 0px 0px 24px; position: relative; white-space: pre-wrap; word-wrap: break-word; overflow: auto; font-family: "Andale Mono", "Lucida Console", Monaco, Liberation, fixed, monospace; font-size: 11px; line-height: 22px; color: rgb(0, 0, 0); word-break: break-all; border: 1px solid rgb(204, 204, 204); vertical-align: baseline; clear: right; background: rgb(247, 247, 247) !important;"> public interface Account {
void operation();
}</pre>

然后把原來的 Account類定義為一個(gè)實(shí)現(xiàn)類:

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

定義一個(gè) 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(); 
     } 
 }

在這個(gè)簡單的例子里远寸,改造一個(gè)類的一個(gè)方法還好抄淑,如果是變動(dòng)整個(gè)模塊,Decorator 很快就會(huì)演化成另一個(gè)噩夢驰后。動(dòng)態(tài)改變 Java 類就是要解決 AOP 的問題肆资,提供一種得到系統(tǒng)支持的可編程的方法,自動(dòng)化地生成或者增強(qiáng) Java 代碼倡怎。這種技術(shù)已經(jīng)廣泛應(yīng)用于最新的 Java 框架內(nèi)迅耘,如 Hibernate,Spring 等监署。

為什么選擇 ASM 颤专?

最直接的改造 Java 類的方法莫過于直接改寫 class 文件。Java 規(guī)范詳細(xì)說明了 class 文件的格式钠乏,直接編輯字節(jié)碼確實(shí)可以改變 Java 類的行為栖秕。直到今天,還有一些 Java 高手們使用最原始的工具晓避,如 UltraEdit 這樣的編輯器對(duì) class 文件動(dòng)手術(shù)簇捍。是的,這是最直接的方法俏拱,但是要求使用者對(duì) Java class 文件的格式了熟于心:小心地推算出想改造的函數(shù)相對(duì)文件首部的偏移量暑塑,同時(shí)重新計(jì)算 class 文件的校驗(yàn)碼以通過 Java 虛擬機(jī)的安全機(jī)制。

Java 5 中提供的 Instrument 包也可以提供類似的功能:啟動(dòng)時(shí)往 Java 虛擬機(jī)中掛上一個(gè)用戶定義的 hook 程序锅必,可以在裝入特定類的時(shí)候改變特定類的字節(jié)碼事格,從而改變該類的行為。但是其缺點(diǎn)也是明顯的:

  • Instrument 包是在整個(gè)虛擬機(jī)上掛了一個(gè)鉤子程序搞隐,每次裝入一個(gè)新類的時(shí)候驹愚,都必須執(zhí)行一遍這段程序,即使這個(gè)類不需要改變劣纲。
  • 直接改變字節(jié)碼事實(shí)上類似于直接改寫 class 文件逢捺,無論是調(diào)用 ClassFileTransformer. transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer),還是 Instrument.redefineClasses(ClassDefinition[] definitions)癞季,都必須提供新 Java 類的字節(jié)碼劫瞳。也就是說,同直接改寫 class 文件一樣绷柒,使用 Instrument 也必須了解想改造的方法相對(duì)類首部的偏移量志于,才能在適當(dāng)?shù)奈恢蒙喜迦胄碌拇a。

盡管 Instrument 可以改造類辉巡,但事實(shí)上恨憎,Instrument 更適用于監(jiān)控和控制虛擬機(jī)的行為。

一種比較理想且流行的方法是使用 java.lang.ref.proxy。我們?nèi)耘f使用上面的例子憔恳,給 Account類加上 checkSecurity 功能 :

首先瓤荔,Proxy 編程是面向接口的。下面我們會(huì)看到钥组,Proxy 并不負(fù)責(zé)實(shí)例化對(duì)象输硝,和 Decorator 模式一樣,要把 Account定義成一個(gè)接口程梦,然后在 AccountImpl里實(shí)現(xiàn) Account接口点把,接著實(shí)現(xiàn)一個(gè) InvocationHandler``Account方法被調(diào)用的時(shí)候,虛擬機(jī)都會(huì)實(shí)際調(diào)用這個(gè)InvocationHandlerinvoke方法:

 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); 
     } 
 }

最后屿附,在應(yīng)用程序中指定 InvocationHandler生成代理對(duì)象:

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 的對(duì)象都必須定義一個(gè)接口,而且用這些對(duì)象的代碼也必須是對(duì)接口編程的:Proxy 生成的對(duì)象是接口一致的而不是對(duì)象一致的:例子中 Proxy.newProxyInstance生成的是實(shí)現(xiàn) Account接口的對(duì)象而不是 AccountImpl的子類挺份。這對(duì)于軟件架構(gòu)設(shè)計(jì)褒翰,尤其對(duì)于既有軟件系統(tǒng)是有一定掣肘的。
  • Proxy 畢竟是通過反射實(shí)現(xiàn)的匀泊,必須在效率上付出代價(jià):有實(shí)驗(yàn)數(shù)據(jù)表明优训,調(diào)用反射比一般的函數(shù)開銷至少要大 10 倍。而且各聘,從程序?qū)崿F(xiàn)上可以看出揣非,對(duì) proxy class 的所有方法調(diào)用都要通過使用反射的 invoke 方法。因此躲因,對(duì)于性能關(guān)鍵的應(yīng)用早敬,使用 proxy class 是需要精心考慮的,以避免反射成為整個(gè)應(yīng)用的瓶頸毛仪。

ASM 能夠通過改造既有類搁嗓,直接生成需要的代碼芯勘。增強(qiáng)的代碼是硬編碼在新生成的類文件內(nèi)部的箱靴,沒有反射帶來性能上的付出。同時(shí)荷愕,ASM 與 Proxy 編程不同衡怀,不需要為增強(qiáng)代碼而新定義一個(gè)接口,生成的代碼可以覆蓋原來的類安疗,或者是原始類的子類抛杨。它是一個(gè)普通的 Java 類而不是 proxy 類,甚至可以在應(yīng)用程序的類框架中擁有自己的位置荐类,派生自己的子類怖现。

相比于其他流行的 Java 字節(jié)碼操縱工具,ASM 更小更快。ASM 具有類似于 BCEL 或者 SERP 的功能屈嗤,而只有 33k 大小潘拨,而后者分別有 350k 和 150k。同時(shí)饶号,同樣類轉(zhuǎn)換的負(fù)載铁追,如果 ASM 是 60% 的話,BCEL 需要 700%茫船,而 SERP 需要 1100% 或者更多琅束。

ASM 已經(jīng)被廣泛應(yīng)用于一系列 Java 項(xiàng)目: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,另一個(gè)更高層一些的自動(dòng)代碼生成工具使用了 ASM府怯。

Java 類文件概述

所謂 Java 類文件刻诊,就是通常用 javac 編譯器產(chǎn)生的 .class 文件。這些文件具有嚴(yán)格定義的格式牺丙。為了更好的理解 ASM则涯,首先對(duì) Java 類文件格式作一點(diǎn)簡單的介紹。Java 源文件經(jīng)過 javac 編譯器編譯之后,將會(huì)生成對(duì)應(yīng)的二進(jìn)制文件(如下圖所示)粟判。每個(gè)合法的 Java 類文件都具備精確的定義肖揣,而正是這種精確的定義,才使得 Java 虛擬機(jī)得以正確讀取和解釋所有的 Java 類文件浮入。

圖 2. ASM – Javac 流程
圖 2. ASM – Javac 流程

Java 類文件是 8 位字節(jié)的二進(jìn)制流龙优。數(shù)據(jù)項(xiàng)按順序存儲(chǔ)在 class 文件中,相鄰的項(xiàng)之間沒有間隔事秀,這使得 class 文件變得緊湊彤断,減少存儲(chǔ)空間。在 Java 類文件中包含了許多大小不同的項(xiàng)易迹,由于每一項(xiàng)的結(jié)構(gòu)都有嚴(yán)格規(guī)定宰衙,這使得 class 文件能夠從頭到尾被順利地解析。下面讓我們來看一下 Java 類文件的內(nèi)部結(jié)構(gòu)睹欲,以便對(duì)此有個(gè)大致的認(rèn)識(shí)供炼。

例如,一個(gè)最簡單的 Hello World 程序:

 public class HelloWorld { 
     public static void main(String[] args) { 
         System.out.println("Hello world"); 
     } 
 }

經(jīng)過 javac 編譯后窘疮,得到的類文件大致是:

圖 3. ASM – Java 類文件
圖 3. ASM – Java 類文件

從上圖中可以看到袋哼,一個(gè) Java 類文件大致可以歸為 10 個(gè)項(xiàng):

  • Magic:該項(xiàng)存放了一個(gè) Java 類文件的魔數(shù)(magic number)和版本信息。一個(gè) Java 類文件的前 4 個(gè)字節(jié)被稱為它的魔數(shù)闸衫。每個(gè)正確的 Java 類文件都是以 0xCAFEBABE 開頭的涛贯,這樣保證了 Java 虛擬機(jī)能很輕松的分辨出 Java 文件和非 Java 文件。
  • Version:該項(xiàng)存放了 Java 類文件的版本信息蔚出,它對(duì)于一個(gè) Java 文件具有重要的意義弟翘。因?yàn)?Java 技術(shù)一直在發(fā)展,所以類文件的格式也處在不斷變化之中骄酗。類文件的版本信息讓虛擬機(jī)知道如何去讀取并處理該類文件稀余。
  • Constant Pool:該項(xiàng)存放了類中各種文字字符串、類名趋翻、方法名和接口名稱睛琳、final 變量以及對(duì)外部類的引用信息等常量。虛擬機(jī)必須為每一個(gè)被裝載的類維護(hù)一個(gè)常量池嘿歌,常量池中存儲(chǔ)了相應(yīng)類型所用到的所有類型掸掏、字段和方法的符號(hào)引用茁影,因此它在 Java 的動(dòng)態(tài)鏈接中起到了核心的作用宙帝。常量池的大小平均占到了整個(gè)類大小的 60% 左右。
  • Access_flag:該項(xiàng)指明了該文件中定義的是類還是接口(一個(gè) class 文件中只能有一個(gè)類或接口)募闲,同時(shí)還指名了類或接口的訪問標(biāo)志步脓,如 public,private, abstract 等信息。
  • This Class:指向表示該類全限定名稱的字符串常量的指針靴患。
  • Super Class:指向表示父類全限定名稱的字符串常量的指針仍侥。
  • Interfaces:一個(gè)指針數(shù)組,存放了該類或父類實(shí)現(xiàn)的所有接口名稱的字符串常量的指針鸳君。以上三項(xiàng)所指向的常量农渊,特別是前兩項(xiàng),在我們用 ASM 從已有類派生新類時(shí)一般需要修改:將類名稱改為子類名稱或颊;將父類改為派生前的類名稱砸紊;如果有必要,增加新的實(shí)現(xiàn)接口囱挑。
  • Fields:該項(xiàng)對(duì)類或接口中聲明的字段進(jìn)行了細(xì)致的描述醉顽。需要注意的是,fields 列表中僅列出了本類或接口中的字段平挑,并不包括從超類和父接口繼承而來的字段游添。
  • Methods:該項(xiàng)對(duì)類或接口中聲明的方法進(jìn)行了細(xì)致的描述。例如方法的名稱通熄、參數(shù)和返回值類型等唆涝。需要注意的是,methods 列表里僅存放了本類或本接口中的方法唇辨,并不包括從超類和父接口繼承而來的方法石抡。使用 ASM 進(jìn)行 AOP 編程,通常是通過調(diào)整 Method 中的指令來實(shí)現(xiàn)的助泽。
  • Class attributes:該項(xiàng)存放了在該文件中類或接口所定義的屬性的基本信息啰扛。

事實(shí)上,使用 ASM 動(dòng)態(tài)生成類嗡贺,不需要像早年的 class hacker 一樣隐解,熟知 class 文件的每一段,以及它們的功能诫睬、長度煞茫、偏移量以及編碼方式。ASM 會(huì)給我們照顧好這一切的摄凡,我們只要告訴 ASM 要改動(dòng)什么就可以了 —— 當(dāng)然续徽,我們首先得知道要改什么:對(duì)類文件格式了解的越多,我們就能更好地使用 ASM 這個(gè)利器亲澡。

ASM 3.0 編程框架

ASM 通過樹這種數(shù)據(jù)結(jié)構(gòu)來表示復(fù)雜的字節(jié)碼結(jié)構(gòu)钦扭,并利用 Push 模型來對(duì)樹進(jìn)行遍歷,在遍歷過程中對(duì)字節(jié)碼進(jìn)行修改床绪。所謂的 Push 模型類似于簡單的 Visitor 設(shè)計(jì)模式客情,因?yàn)樾枰幚碜止?jié)碼結(jié)構(gòu)是固定的其弊,所以不需要專門抽象出一種 Vistable 接口,而只需要提供 Visitor 接口膀斋。所謂 Visitor 模式和 Iterator 模式有點(diǎn)類似梭伐,它們都被用來遍歷一些復(fù)雜的數(shù)據(jù)結(jié)構(gòu)。Visitor 相當(dāng)于用戶派出的代表仰担,深入到算法內(nèi)部糊识,由算法安排訪問行程。Visitor 代表可以更換摔蓝,但對(duì)算法流程無法干涉技掏,因此是被動(dòng)的,這也是它和 Iterator 模式由用戶主動(dòng)調(diào)遣算法方式的最大的區(qū)別项鬼。

在 ASM 中哑梳,提供了一個(gè) ClassReader類,這個(gè)類可以直接由字節(jié)數(shù)組或由 class 文件間接的獲得字節(jié)碼數(shù)據(jù)绘盟,它能正確的分析字節(jié)碼鸠真,構(gòu)建出抽象的樹在內(nèi)存中表示字節(jié)碼。它會(huì)調(diào)用 accept方法龄毡,這個(gè)方法接受一個(gè)實(shí)現(xiàn)了 ClassVisitor接口的對(duì)象實(shí)例作為參數(shù)吠卷,然后依次調(diào)用ClassVisitor接口的各個(gè)方法。字節(jié)碼空間上的偏移被轉(zhuǎn)換成 visit 事件時(shí)間上調(diào)用的先后沦零,所謂 visit 事件是指對(duì)各種不同 visit 函數(shù)的調(diào)用祭隔,ClassReader知道如何調(diào)用各種 visit 函數(shù)。在這個(gè)過程中用戶無法對(duì)操作進(jìn)行干涉路操,所以遍歷的算法是確定的疾渴,用戶可以做的是提供不同的 Visitor 來對(duì)字節(jié)碼樹進(jìn)行不同的修改。ClassVisitor會(huì)產(chǎn)生一些子過程屯仗,比如 visitMethod會(huì)返回一個(gè)實(shí)現(xiàn) MethordVisitor接口的實(shí)例搞坝,visitField會(huì)返回一個(gè)實(shí)現(xiàn) FieldVisitor接口的實(shí)例,完成子過程后控制返回到父過程魁袜,繼續(xù)訪問下一節(jié)點(diǎn)桩撮。因此對(duì)于ClassReader來說,其內(nèi)部順序訪問是有一定要求的峰弹。實(shí)際上用戶還可以不通過 ClassReader類店量,自行手工控制這個(gè)流程,只要按照一定的順序鞠呈,各個(gè) visit 事件被先后正確的調(diào)用融师,最后就能生成可以被正確加載的字節(jié)碼。當(dāng)然獲得更大靈活性的同時(shí)也加大了調(diào)整字節(jié)碼的復(fù)雜度粟按。

各個(gè) ClassVisitor通過職責(zé)鏈 (Chain-of-responsibility) 模式诬滩,可以非常簡單的封裝對(duì)字節(jié)碼的各種修改霹粥,而無須關(guān)注字節(jié)碼的字節(jié)偏移灭将,因?yàn)檫@些實(shí)現(xiàn)細(xì)節(jié)對(duì)于用戶都被隱藏了疼鸟,用戶要做的只是覆寫相應(yīng)的 visit 函數(shù)。

ClassAdaptor類實(shí)現(xiàn)了 ClassVisitor接口所定義的所有函數(shù)庙曙,當(dāng)新建一個(gè) ClassAdaptor對(duì)象的時(shí)候空镜,需要傳入一個(gè)實(shí)現(xiàn)了ClassVisitor接口的對(duì)象,作為職責(zé)鏈中的下一個(gè)訪問者 (Visitor)捌朴,這些函數(shù)的默認(rèn)實(shí)現(xiàn)就是簡單的把調(diào)用委派給這個(gè)對(duì)象吴攒,然后依次傳遞下去形成職責(zé)鏈。當(dāng)用戶需要對(duì)字節(jié)碼進(jìn)行調(diào)整時(shí)砂蔽,只需從 ClassAdaptor類派生出一個(gè)子類洼怔,覆寫需要修改的方法,完成相應(yīng)功能后再把調(diào)用傳遞下去左驾。這樣镣隶,用戶無需考慮字節(jié)偏移,就可以很方便的控制字節(jié)碼诡右。

每個(gè) ClassAdaptor類的派生類可以僅封裝單一功能安岂,比如刪除某函數(shù)、修改字段可見性等等帆吻,然后再加入到職責(zé)鏈中域那,這樣耦合更小,重用的概率也更大猜煮,但代價(jià)是產(chǎn)生很多小對(duì)象次员,而且職責(zé)鏈的層次太長的話也會(huì)加大系統(tǒng)調(diào)用的開銷,用戶需要在低耦合和高效率之間作出權(quán)衡王带。用戶可以通過控制職責(zé)鏈中 visit 事件的過程翠肘,對(duì)類文件進(jìn)行如下操作:

  1. 刪除類的字段、方法辫秧、指令:只需在職責(zé)鏈傳遞過程中中斷委派束倍,不訪問相應(yīng)的 visit 方法即可,比如刪除方法時(shí)只需直接返回 null盟戏,而不是返回由 visitMethod方法返回的 MethodVisitor對(duì)象绪妹。
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); 
         } 
     }

  1. 修改類、字段柿究、方法的名字或修飾符:在職責(zé)鏈傳遞過程中替換調(diào)用參數(shù)邮旷。
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); 
        } 
     }

  1. 增加新的類、方法蝇摸、字段

ASM 的最終的目的是生成可以被正常裝載的 class 文件婶肩,因此其框架結(jié)構(gòu)為客戶提供了一個(gè)生成字節(jié)碼的工具類 —— ClassWriter办陷。它實(shí)現(xiàn)了 ClassVisitor接口,而且含有一個(gè) toByteArray()函數(shù)律歼,返回生成的字節(jié)碼的字節(jié)流民镜,將字節(jié)流寫回文件即可生產(chǎn)調(diào)整后的 class 文件。一般它都作為職責(zé)鏈的終點(diǎn)险毁,把所有 visit 事件的先后調(diào)用(時(shí)間上的先后)制圈,最終轉(zhuǎn)換成字節(jié)碼的位置的調(diào)整(空間上的前后),如下例:

 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 的時(shí)序圖如下:

圖 4. ASM – 時(shí)序圖
圖 4. ASM – 時(shí)序圖

點(diǎn)擊查看大圖

使用 ASM3.0 進(jìn)行 AOP 編程

我們還是用上面的例子鲸鹦,給 Account類加上 security check 的功能。與 proxy 編程不同跷跪,ASM 不需要將 Account聲明成接口馋嗜,Account可以仍舊是一個(gè)實(shí)現(xiàn)類。ASM 將直接在 Account類上動(dòng)手術(shù)吵瞻,給 Account類的 operation方法首部加上對(duì)SecurityChecker.checkSecurity的調(diào)用葛菇。

首先,我們將從 ClassAdapter繼承一個(gè)類听皿。ClassAdapter是 ASM 框架提供的一個(gè)默認(rèn)類熟呛,負(fù)責(zé)溝通 ClassReaderClassWriter。如果想要改變 ClassReader處讀入的類尉姨,然后從 ClassWriter處輸出庵朝,可以重寫相應(yīng)的 ClassAdapter函數(shù)。這里又厉,為了改變 Account類的 operation 方法九府,我們將重寫 visitMethdod方法。

class AddSecurityCheckClassAdapter extends ClassAdapter {

    public AddSecurityCheckClassAdapter(ClassVisitor cv) {
        //Responsechain 的下一個(gè) ClassVisitor覆致,這里我們將傳入 ClassWriter侄旬,
        // 負(fù)責(zé)改寫后代碼的輸出
        super(cv); 
    } 

    // 重寫 visitMethod,訪問到 "operation" 方法時(shí)煌妈,
    // 給出自定義 MethodVisitor儡羔,實(shí)際改寫方法內(nèi)容
    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) { 
            // 對(duì)于 "operation" 方法
            if (name.equals("operation")) { 
                // 使用自定義 MethodVisitor,實(shí)際改寫方法內(nèi)容
                wrappedMv = new AddSecurityCheckMethodAdapter(mv); 
            } 
        } 
        return wrappedMv; 
    } 
}

下一步就是定義一個(gè)繼承自 MethodAdapterAddSecurityCheckMethodAdapter璧诵,在“operation”方法首部插入對(duì)SecurityChecker.checkSecurity()的調(diào)用汰蜘。

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

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

其中,ClassReader讀到每個(gè)方法的首部時(shí)調(diào)用 visitCode()之宿,在這個(gè)重寫方法里族操,我們用visitMethodInsn(Opcodes.INVOKESTATIC, "SecurityChecker","checkSecurity", "()V");插入了安全檢查功能。

最后比被,我們將集成上面定義的 ClassAdapter色难,ClassReaderClassWriter產(chǎn)生修改后的 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(); 
     } 
 }



執(zhí)行完這段程序后泼舱,我們會(huì)得到一個(gè)新的 Account.class 文件,如果我們使用下面代碼:

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


使用這個(gè) Account枷莉,我們會(huì)得到下面的輸出:

SecurityChecker.checkSecurity ...
operation...


也就是說娇昙,在 `Account`原來的 `operation`內(nèi)容執(zhí)行之前,進(jìn)行了 `SecurityChecker.checkSecurity()`檢查依沮。

### 將動(dòng)態(tài)生成類改造成原始類 Account 的子類

上面給出的例子是直接改造 `Account`類本身的涯贞,從此 `Account`類的 `operation`方法必須進(jìn)行 checkSecurity 檢查枪狂。但事實(shí)上危喉,我們有時(shí)仍希望保留原來的 `Account`類,因此把生成類定義為原始類的子類是更符合 AOP 原則的做法州疾。下面介紹如何將改造后的類定義為 `Account`的子類`Account$EnhancedByASM`辜限。其中主要有兩項(xiàng)工作 :

*   改變 Class Description, 將其命名為 `Account$EnhancedByASM`,將其父類指定為 `Account`严蓖。
*   改變構(gòu)造函數(shù)薄嫡,將其中對(duì)父類構(gòu)造函數(shù)的調(diào)用轉(zhuǎn)換為對(duì) `Account`構(gòu)造函數(shù)的調(diào)用。

在 `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);
}

改進(jìn) `visitMethod`方法,增加對(duì)構(gòu)造函數(shù)的處理:

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`將負(fù)責(zé)把 `Account`的構(gòu)造函數(shù)改造成其子類 `Account$EnhancedByASM`的構(gòu)造函數(shù):

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) { 
     // 調(diào)用父類的構(gòu)造函數(shù)時(shí)
     if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { 
         owner = superClassName; 
     } 
     super.visitMethodInsn(opcode, owner, name, desc);// 改寫父類為 superClassName 
 } 

}


最后演示一下如何在運(yùn)行時(shí)產(chǎn)生并裝入產(chǎn)生的 `Account$EnhancedByASM`毒姨。 我們定義一個(gè) `Util` 類哑蔫,作為一個(gè)類工廠負(fù)責(zé)產(chǎn)生有安全檢查的`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());
    } 
} 

}



靜態(tài)方法 `SecureAccountGenerator.generateSecureAccount()`在運(yùn)行時(shí)動(dòng)態(tài)生成一個(gè)加上了安全檢查的 `Account`子類。著名的 Hibernate 和 Spring 框架弧呐,就是使用這種技術(shù)實(shí)現(xiàn)了 AOP 的“無損注入”闸迷。


## 小結(jié)

最后,我們比較一下 ASM 和其他實(shí)現(xiàn) AOP 的底層技術(shù):

##### 表 1\. AOP 底層技術(shù)比較

| AOP 底層技術(shù) | 功能 | 性能 | 面向接口編程 | 編程難度 |
| --- | --- | --- | --- | --- |
| 直接改寫 class 文件 | 完全控制類 | 無明顯性能代價(jià) | 不要求 | 高俘枫,要求對(duì) class 文件結(jié)構(gòu)和 Java 字節(jié)碼有深刻了解 |
| JDK Instrument | 完全控制類 | 無論是否改寫腥沽,每個(gè)類裝入時(shí)都要執(zhí)行 hook 程序 | 不要求 | 高,要求對(duì) class 文件結(jié)構(gòu)和 Java 字節(jié)碼有深刻了解 |
| JDK Proxy | 只能改寫 method | 反射引入性能代價(jià) | 要求 | 低 |
| ASM | 幾乎能完全控制類 | 無明顯性能代價(jià) | 不要求 | 中鸠蚪,能操縱需要改寫部分的 Java 字節(jié)碼 |

## 參考資料

*   Download [ASM 3.0.](http://forge.objectweb.org/projects/asm/)
*   [Tutorial for ASM 2.0 ](http://asm.objectweb.org/doc/tutorial-asm-2.0.html),the latest document version for How to use ASM.
*   In [Java VM Spec](http://java.sun.com/docs/books/vmspec/2nd-edition/html/ClassFile.doc.html), get detailed information of the Java Class File format.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末今阳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子茅信,更是在濱河造成了極大的恐慌盾舌,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汹押,死亡現(xiàn)場離奇詭異矿筝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)棚贾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門窖维,熙熙樓的掌柜王于貴愁眉苦臉地迎上來榆综,“玉大人,你說我怎么就攤上這事铸史”谴” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵琳轿,是天一觀的道長判沟。 經(jīng)常有香客問我,道長崭篡,這世上最難降的妖魔是什么挪哄? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮琉闪,結(jié)果婚禮上迹炼,老公的妹妹穿的比我還像新娘。我一直安慰自己颠毙,他們只是感情好斯入,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛀蜜,像睡著了一般刻两。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上滴某,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天磅摹,我揣著相機(jī)與錄音,去河邊找鬼壮池。 笑死偏瓤,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的椰憋。 我是一名探鬼主播厅克,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼橙依!你這毒婦竟也來了证舟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤窗骑,失蹤者是張志新(化名)和其女友劉穎女责,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體创译,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抵知,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片刷喜。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡残制,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掖疮,到底是詐尸還是另有隱情初茶,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布浊闪,位于F島的核電站恼布,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏搁宾。R本人自食惡果不足惜折汞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望猛铅。 院中可真熱鬧字支,春花似錦凤藏、人聲如沸奸忽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽栗菜。三九已至,卻和暖如春蹄梢,著一層夾襖步出監(jiān)牢的瞬間疙筹,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國打工禁炒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留而咆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓幕袱,卻偏偏與公主長得像暴备,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子们豌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344