AOP概述
軟件的編程語言最終的目的就是用更自然更靈活的方式模擬世界,從原始機器語言到過程語言再到面向?qū)ο蟮恼Z言,我們看到編程語言在一步步用更自然、更強大的方式描述軟件。AOP是軟件開發(fā)思想的一個飛躍,AOP的引入將有效彌補OOP的不足,OOP和AOP分別從縱向和橫向?qū)浖M(jìn)行抽象,有效地消除重復(fù)性的代碼,使代碼以更優(yōu)雅的更有效的方式進(jìn)行邏輯表達(dá)。
AOP有三種植入切面的方法:其一是編譯期織入,這要求使用特殊的Java編譯器,AspectJ是其中的代表者;其二是類裝載期織入,而這要求使用特殊的類裝載器,AspectJ和AspectWerkz是其中的代表者;其三為動態(tài)代理織入,在運行期為目標(biāo)類添加增強生成子類的方式,Spring AOP采用動態(tài)代理織入切面。
Spring AOP使用了兩種代理機制,一種是基于JDK的動態(tài)代理,另一種是基于CGLib的動態(tài)代理,之所以需要兩種代理機制,很大程度上是因為JDK本身只提供基于接口的代理,不支持類的代理。
基于JDK的代理和基于CGLib的代理是Spring AOP的核心實現(xiàn)技術(shù),認(rèn)識這兩代理技術(shù),有助于探究Spring AOP的實現(xiàn)機理。只要你愿意,你甚至可以拋開Spring,提供自己的AOP實現(xiàn)。
帶有橫切邏輯的實例
???
首先,我們來看一個無法通過OOP進(jìn)行抽象的重復(fù)代碼邏輯,它們就是AOP改造的主要對象。下面,我們通過一個業(yè)務(wù)方法性能監(jiān)視的實例了解橫切邏輯。業(yè)務(wù)方法性能監(jiān)視,在每一個業(yè)務(wù)方法調(diào)用之前開始監(jiān)視,業(yè)務(wù)方法結(jié)束后結(jié)束監(jiān)視并給出性能報告:
代碼清單 2 ForumService:包含性能監(jiān)視橫切代碼
package com.baobaotao.proxy; public class ForumServiceImpl implements ForumService ...{
?public void removeTopic(int topicId) ...{ ????????? //開始性能監(jiān)視 ? PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeTopic"); ? System.out.println("模擬刪除Topic記錄:"+topicId); ? try ...{ ?? Thread.currentThread().sleep(20); ? } catch (Exception e) ...{ ?? throw new RuntimeException(e); ? } ? //結(jié)束監(jiān)視、并給出性能報告信息 ? PerformanceMonitor.end(); ?}
?public void removeForum(int forumId) ...{ ? //開始性能監(jiān)視 PerformanceMonitor.begin("com.baobaotao.proxy.ForumServiceImpl.removeForum"); ? System.out.println("模擬刪除Forum記錄:"+forumId); ? try ...{ ?? Thread.currentThread().sleep(40); ? } catch (Exception e) ...{ ?? throw new RuntimeException(e); ? } ???????? //結(jié)束監(jiān)視、并給出性能報告信息 ? PerformanceMonitor.end(); ?} }
|
代碼清單 2中粗體表示的代碼就是具有橫切特征的代碼,需要進(jìn)行性能監(jiān)視的每個業(yè)務(wù)方法的前后都需要添加類似的性能監(jiān)視語句。
???
我們保證實例的完整性,我們提供了一個非常簡單的性能監(jiān)視實現(xiàn)類,如所示代碼清單 3所示:
代碼清單 3 PerformanceMonitor
package com.baobaotao.proxy;
public class PerformanceMonitor { ? //通過一個ThreadLocal保存線程相關(guān)的性能監(jiān)視信息 ?private static ThreadLocal<MethodPerformace> performaceRecord = new ThreadLocal<MethodPerformace>(); ?public static void begin(String method) { ? System.out.println("begin monitor..."); ? MethodPerformace mp = new MethodPerformace(method); ? performaceRecord.set(mp); ?} ?public static void end() { ? System.out.println("end monitor..."); ? MethodPerformace mp = performaceRecord.get(); ? mp.printPerformace(); //打印出業(yè)務(wù)方法性能監(jiān)視的信息 ?} }
|
PerformanceMonitor提供了兩個方法,begin(String method)方法開始對某個業(yè)務(wù)類方法的監(jiān)視,method為業(yè)務(wù)方法的簽名,而end()方法結(jié)束對業(yè)務(wù)方法的監(jiān)視,并給出性能監(jiān)視的信息。由于每一個業(yè)務(wù)方法都必須單獨記錄性能監(jiān)視數(shù)據(jù),所以我們使用了ThreadLocal,ThreadLocal是削除非線程安全狀態(tài)的不二法寶。ThreadLocal中的元素為方法性能記錄對象MethodPerformace,它的代碼如下所示:
代碼清單 4 MethodPerformace
package com.baobaotao.proxy; public class MethodPerformace { ?private long begin; ?private long end; ?private String serviceMethod; ??? public MethodPerformace(String serviceMethod){ ?????? this.serviceMethod = serviceMethod; ?????? this.begin = System.currentTimeMillis();//記錄方法調(diào)用開始時的系統(tǒng)時間 ??? } ??? public void printPerformace(){ ??????? //以下兩行程序得到方法調(diào)用后的系統(tǒng)時間,并計算出方法執(zhí)行花費時間 ??????? end = System.currentTimeMillis(); ??????? long elapse = end - begin; ??????? //報告業(yè)務(wù)方法執(zhí)行時間 ??????? System.out.println(serviceMethod+"花費"+elapse+"毫秒。"); ??? } }
|
#p#
通過下面代碼測試這個擁有方法性能監(jiān)視能力的業(yè)務(wù)方法:
package com.baobaotao.proxy; public class TestForumService { ?public static void main(String[] args) { ??????? ForumService forumService = new ForumServiceImpl(); ??????? forumService .removeForum(10); ??? forumService .removeTopic(1012); ?} }
|
我們得到以下的輸出信息:
begin monitor... 模擬刪除Forum記錄:10 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeForum花費47毫秒。
begin monitor... 模擬刪除Topic記錄:1012 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeTopic花費16毫秒。
|
如實例所示,要對業(yè)務(wù)類進(jìn)行性能監(jiān)視,就必須在每個業(yè)務(wù)類方法的前后兩處添加上重復(fù)性的開啟性能監(jiān)視和結(jié)束性能監(jiān)視的代碼。這些非業(yè)務(wù)邏輯的性能監(jiān)視代碼破壞了作為業(yè)務(wù)類ForumServiceImpl的純粹性。下面,我們分別JDK動態(tài)代理和CGLib動態(tài)代理技術(shù),將業(yè)務(wù)方法中開啟和結(jié)束性能監(jiān)視的這些橫切代碼從業(yè)務(wù)類中完成移除。
JDK動態(tài)代理
???
在JDK 1.3以后提供了動態(tài)代理的技術(shù),允許開發(fā)者在運行期創(chuàng)建接口的代理實例。在Sun剛推出動態(tài)代理時,還很難想象它有多大的實際用途,現(xiàn)在我們終于發(fā)現(xiàn)動態(tài)代理是實現(xiàn)AOP的絕好底層技術(shù)。
???
JDK的動態(tài)代理主要涉及到j(luò)ava.lang.reflect包中的兩個類:Proxy和InvocationHandler。其中InvocationHandler是一個接口,可以通過實現(xiàn)該接口定義橫切邏輯,在并通過反射機制調(diào)用目標(biāo)類的代碼,動態(tài)將橫切邏輯和業(yè)務(wù)邏輯編織在一起。
??
而Proxy為InvocationHandler實現(xiàn)類動態(tài)創(chuàng)建一個符合某一接口的代理實例。這樣講一定很抽象,我們馬上著手動用Proxy和InvocationHandler這兩個魔法戒對上一節(jié)中的性能監(jiān)視代碼進(jìn)行AOP式的改造。
???
首先,我們從業(yè)務(wù)類ForumServiceImpl 中刪除性能監(jiān)視的橫切代碼,使ForumServiceImpl只負(fù)責(zé)具體的業(yè)務(wù)邏輯,如所示:
代碼清單 5 ForumServiceImpl:移除性能監(jiān)視橫切代碼
package com.baobaotao.proxy; public class ForumServiceImpl implements ForumService { ?public void removeTopic(int topicId) { ???????? ① ? System.out.println("模擬刪除Topic記錄:"+topicId); ? try { ?? Thread.currentThread().sleep(20); ? } catch (Exception e) { ?? throw new RuntimeException(e); ? } ?? ② ?} ?public void removeForum(int forumId) { ???????? ① ? System.out.println("模擬刪除Forum記錄:"+forumId); ? try { ?? Thread.currentThread().sleep(40); ? } catch (Exception e) { ?? throw new RuntimeException(e); ? } ???????? ② ?} }
|
在代碼清單 5中的①和②處,原來的性能監(jiān)視代碼被移除了,我們只保留了真正的業(yè)務(wù)邏輯。
???
從業(yè)務(wù)類中移除的橫切代碼當(dāng)然還得找到一個寄居之所,InvocationHandler就是橫切代碼的家園樂土,我們將性能監(jiān)視的代碼安置在PerformaceHandler中,如代碼清單 6所示:
代碼清單 6 PerformaceHandler
package com.baobaotao.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method;
public class PerformaceHandler implements InvocationHandler { ??? private Object target; ?public PerformaceHandler(Object target){//①target為目標(biāo)的業(yè)務(wù)類 ? this.target = target; ?} ?public Object invoke(Object proxy, Method method, Object[] args) ?? throws Throwable { ? PerformanceMonitor.begin(target.getClass().getName()+"."+ method.getName()); ? Object obj = method.invoke(target, args);//②通過反射方法調(diào)用目標(biāo)業(yè)務(wù)類的業(yè)務(wù)方法 ? PerformanceMonitor.end(); ? return obj; ?} }
|
粗體部分的代碼為性能監(jiān)視的橫切代碼,我們發(fā)現(xiàn),橫切代碼只出現(xiàn)一次,而不是原來那樣星灑各處。大家注意②處的method.invoke(),該語句通過反射的機制調(diào)用目標(biāo)對象的方法,這樣InvocationHandler的invoke(Object proxy, Method method, Object[] args)方法就將橫切代碼和目標(biāo)業(yè)務(wù)類代碼編織到一起了,所以我們可以將InvocationHandler看成是業(yè)務(wù)邏輯和橫切邏輯的編織器。下面,我們對這段代碼做進(jìn)一步的說明。
#p#
首先,我們實現(xiàn)InvocationHandler接口,該接口定義了一個 invoke(Object proxy, Method method, Object[] args)的方法,proxy是代理實例,一般不會用到;method是代理實例上的方法,通過它可以發(fā)起對目標(biāo)類的反射調(diào)用;args是通過代理類傳入的方法參數(shù),在反射調(diào)用時使用。
???
此外,我們在構(gòu)造函數(shù)里通過target傳入真實的目標(biāo)對象,如①處所示,在接口方法invoke(Object proxy, Method method, Object[] args)里,將目標(biāo)類實例傳給method.invoke()方法,通過反射調(diào)用目標(biāo)類方法,如②所示。
???
下面,我們通過Proxy結(jié)合PerformaceHandler創(chuàng)建ForumService接口的代理實例,如代碼清單 7所示:
代碼清單 7 TestForumService:創(chuàng)建代理實例
package com.baobaotao.proxy; import java.lang.reflect.Proxy; public class TestForumService { ?public static void main(String[] args) { ? ForumService target = new ForumServiceImpl();//①目標(biāo)業(yè)務(wù)類 //② 將目標(biāo)業(yè)務(wù)類和橫切代碼編織到一起 ? PerformaceHandler handler = new PerformaceHandler(target); ???????? //③為編織了目標(biāo)業(yè)務(wù)類邏輯和性能監(jiān)視橫切邏輯的handler創(chuàng)建代理類 ? ForumService proxy = (ForumService) Proxy.newProxyInstance( target.getClass().getClassLoader(), ??? target.getClass().getInterfaces(), ?handler); ???????? //④ 操作代理實例 ? proxy.removeForum(10); ? proxy.removeTopic(1012); ?} }
|
上面的代碼完成了業(yè)務(wù)類代碼和橫切代碼編織和接口代理實例生成的工作,其中在②處,我們將ForumService實例編織為一個包含性能監(jiān)視邏輯的PerformaceHandler實例,然后在③處,通過Proxy的靜態(tài)方法newProxyInstance()為融合了業(yè)務(wù)類邏輯和性能監(jiān)視邏輯的handler創(chuàng)建一個ForumService接口的代理實例,該方法的第一個入?yún)轭惣虞d器,第二個入?yún)閯?chuàng)建的代理實例所要實現(xiàn)的一組接口,第三個參數(shù)是整合了業(yè)務(wù)邏輯和橫切邏輯的編織器對象。
按照③處的設(shè)置方式,這個代理實例就實現(xiàn)了目標(biāo)業(yè)務(wù)類的所有接口,也即ForumServiceImpl的ForumService接口。這樣,我們就可以按照調(diào)用ForumService接口的實例相同的方式調(diào)用代理實例,如④所示。運行以上的代碼,輸出以下的信息:
begin monitor... 模擬刪除Forum記錄:10 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeForum花費47毫秒。
begin monitor... 模擬刪除Topic記錄:1012 end monitor... com.baobaotao.proxy.ForumServiceImpl.removeTopic花費26毫秒。
|
我們發(fā)現(xiàn),程序的運行效果和直接在業(yè)務(wù)類中編寫性能監(jiān)視邏輯的效果一致,但是在這里,原來分散的橫切邏輯代碼已經(jīng)被我們抽取到PerformaceHandler中。當(dāng)其它業(yè)務(wù)類(如UserService、SystemService等)的業(yè)務(wù)方法也需要使用性能監(jiān)視時,我們只要按照以上的方式,分別為它們創(chuàng)建代理對象就可以了。下面,我們用時序圖描述調(diào)用關(guān)系,進(jìn)一步代理實例的本質(zhì),如圖1所示:
?
圖1:代理實例的時序圖
???
我們在上圖中特別使用虛線陰影的方式對通過代理器創(chuàng)建的ForumService實例進(jìn)行凸顯,該實例內(nèi)部利用PerformaceHandler整合橫切邏輯和業(yè)務(wù)邏輯。調(diào)用者調(diào)用代理對象的的removeForum()和removeTopic()方法時,上圖的內(nèi)部調(diào)用時序清晰地告訴了我們實際上所發(fā)生的一切。
CGLib動態(tài)代理
??
使用JDK創(chuàng)建代理有一個限制,即它只能為接口創(chuàng)建代理,這一點我們從Proxy的接口方法newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)就看得很清楚,第三個入?yún)nterfaces就是為代理實例指定的實現(xiàn)接口。雖然,面向接口的編程被很多很有影響力人(包括Rod Johnson)的推崇,但在實際開發(fā)中,開發(fā)者也遇到了很多困惑:難道對一個簡單業(yè)務(wù)表的操作真的需要創(chuàng)建5個類(領(lǐng)域?qū)ο箢悺ao接口,Dao實現(xiàn)類,Service接口和Service實現(xiàn)類)嗎?對于這一問題,我們還是留待大家進(jìn)一步討論。現(xiàn)在的問題是:對于沒有通過接口定義業(yè)務(wù)方法的類,如何動態(tài)創(chuàng)建代理實例呢?JDK的代理技術(shù)顯然已經(jīng)黔驢技窮,CGLib作為一個替代者,填補了這個空缺。你可以從http://cglib.sourceforge.net/獲取CGLib的類包,也可以直接從Spring的關(guān)聯(lián)類庫lib/cglib中獲取類包。
#p#
??
CGLib采用非常底層的字節(jié)碼技術(shù),可以為一個類創(chuàng)建子類,并在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用,并在攔截方法相應(yīng)地織入橫切邏輯。下面,我們采用CGLib技術(shù),編寫一個可以為任何類創(chuàng)建織入性能監(jiān)視橫切邏輯的代理對象的代理器,如代碼清單 8所示:
代碼清單 8 CglibProxy
package com.baobaotao.proxy; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy;
public class CglibProxy implements MethodInterceptor { ?private Enhancer enhancer = new Enhancer(); ?public Object getProxy(Class clazz) { ? enhancer.setSuperclass(clazz); ① 設(shè)置需要創(chuàng)建子類的類 ? enhancer.setCallback(this); ? return enhancer.create(); ②通過字節(jié)碼技術(shù)動態(tài)創(chuàng)建子類實例 ?} ?public Object intercept(Object obj, Method method, Object[] args, ?? MethodProxy proxy) throws Throwable { ? PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName()); ? Object result=proxy.invokeSuper(obj, args); ③ 通過代理類調(diào)用父類中的方法 ? PerformanceMonitor.end(); ? return result; ?} }
|
在上面代碼中,你可以通過getProxy(Class clazz)為一個類創(chuàng)建動態(tài)代理對象,該代理對象是指定類clazz的子類。在這個代理對象中,我們織入性能監(jiān)視的橫切邏輯(粗體部分)。intercept(Object obj, Method method, Object[] args,MethodProxy proxy)是CGLib定義的Inerceptor接口的方法,obj表示父類的實例,method為父類方法的反射對象,args為方法的動態(tài)入?yún)ⅲ鴓roxy為代理類實例。
???
下面,我們通過CglibProxy為ForumServiceImpl類創(chuàng)建代理對象,并測試代理對象的方法,如代碼清單 9所示:
代碼清單 9 TestForumService:測試Cglib創(chuàng)建的代理類
package com.baobaotao.proxy; import java.lang.reflect.Proxy; public class TestForumService { ?public static void main(String[] args) { ?? CglibProxy proxy = new CglibProxy(); ?? ForumServiceImpl forumService = //① 通過動態(tài)生成子類的方式創(chuàng)建代理對象 (ForumServiceImpl )proxy.getProxy(ForumServiceImpl.class); ?? forumService.removeForum(10); ?? forumService.removeTopic(1023); ?} }
|
在①中,我們通過CglibProxy為ForumServiceImpl動態(tài)創(chuàng)建了一個織入性能監(jiān)視邏輯的代理對象,并調(diào)用了代理對象的業(yè)務(wù)方法。運行上面的代碼,輸入以下的信息:
begin monitor... 模擬刪除Forum記錄:10 end monitor... com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeForum花費47毫秒。 begin monitor... 模擬刪除Topic記錄:1023 end monitor... com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeTopic花費16毫秒。
|
觀察以上的輸出,除了發(fā)現(xiàn)兩個業(yè)務(wù)方法中都織入了性能監(jiān)控的邏輯外,我們還發(fā)現(xiàn)代理類的名字是com.baobaotao.proxy.ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0,這個特殊的類就是CGLib為ForumServiceImpl所動態(tài)創(chuàng)建的子類。
小結(jié)
?????
Spring AOP在底層就是利用JDK動態(tài)代理或CGLib動態(tài)代理技術(shù)為目標(biāo)Bean織入橫切邏輯。在這里,我們對以上兩節(jié)動態(tài)創(chuàng)建代理對象做一個小結(jié)。
在PerformaceHandler和CglibProxy中,有三點值得注意的地方是:第一,目標(biāo)類的所有方法都被添加了性能監(jiān)視橫切的代碼,而有時,這并不是我們所期望的,我們可能只希望對業(yè)務(wù)類中的某些方法織入橫切代碼;第二,我們手工指定了織入橫切代碼的織入點,即在目標(biāo)類業(yè)務(wù)方法的開始和結(jié)束前調(diào)用;第三,我們手工編寫橫切代碼。以上三個問題,在AOP中占用重要的地位,因為Spring AOP的主要工作就是圍繞以上三點展開:Spring AOP通過Pointcut(切點)指定在哪些類的哪些方法上施加橫切邏輯,通過Advice(增強)描述橫切邏輯和方法的具體織入點(方法前、方法后、方法的兩端等),此外,Spring還通過Advisor(切面)組合Pointcut和Advice。有了Advisor的信息,Spring就可以利用JDK或CGLib的動態(tài)代理技術(shù)為目標(biāo)Bean創(chuàng)建織入切面的代理對象了。
JDK動態(tài)代理所創(chuàng)建的代理對象,在JDK 1.3下,性能強差人意。雖然在高版本的JDK中,動態(tài)代理對象的性能得到了很大的提高,但是有研究表明,CGLib所創(chuàng)建的動態(tài)代理對象的性能依舊比JDK的所創(chuàng)建的代理對象的性能高不少(大概10倍)。而CGLib在創(chuàng)建代理對象時性能卻比JDK動態(tài)代理慢很多(大概8倍),所以對于singleton的代理對象或者具有實例池的代理,因為不需要頻繁創(chuàng)建代理對象,所以比較適合用CGLib動態(tài)代理技術(shù),反之適合用JDK動態(tài)代理技術(shù)。此外,由于CGLib采用生成子類的技術(shù)創(chuàng)建代理對象,所以不能對目標(biāo)類中的final方法進(jìn)行代理。