轉(zhuǎn)自:我叫劉半仙
https://my.oschina.net/liughDevelop/blog/1457097
為什么會(huì)有面向切面編程(AOP)?我們知道Java是一個(gè)面向?qū)ο螅∣OP)的語(yǔ)言,但它有一些弊端,比如當(dāng)我們需要為多個(gè)不具有繼承關(guān)系的對(duì)象引入一個(gè)公共行為,例如日志、權(quán)限驗(yàn)證、事務(wù)等功能時(shí),只能在在每個(gè)對(duì)象里引用公共行為。這樣做不便于維護(hù),而且有大量重復(fù)代碼。AOP的出現(xiàn)彌補(bǔ)了OOP的這點(diǎn)不足。
為了闡述清楚Spring AOP,我們從將以下方面進(jìn)行討論:
代理模式
靜態(tài)代理原理及實(shí)踐
動(dòng)態(tài)代理原理及實(shí)踐
Spring AOP原理及實(shí)戰(zhàn)
1. 代理模式
代理模式:為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問(wèn)。這段話比較官方,但我更傾向于用自己的語(yǔ)言理解:比如A對(duì)象要做一件事情,在沒(méi)有代理前,自己來(lái)做;在對(duì) A 代理后,由 A 的代理類 B 來(lái)做。代理其實(shí)是在原實(shí)例前后加了一層處理,這也是 AOP 的初級(jí)輪廓。
2. 靜態(tài)代理原理及實(shí)踐
靜態(tài)代理模式:靜態(tài)代理說(shuō)白了,就是在程序運(yùn)行前就已經(jīng)存在代理類的字節(jié)碼文件、代理類和原始類的關(guān)系在運(yùn)行前就已經(jīng)確定。廢話不多說(shuō),我們看一下代碼。為了方便閱讀,博主把單獨(dú)的 class 文件合并到接口中,讀者可以直接復(fù)制代碼運(yùn)行:
package test.staticProxy; // 接口public interface IUserDao { void save(); void find();} //目標(biāo)對(duì)象class UserDao implements IUserDao{ @Override public void save() { System.out.println("模擬:保存用戶!"); } @Override public void find() { System.out.println("模擬:查詢用戶"); }} /** * 靜態(tài)代理 * 特點(diǎn): * 2\. 目標(biāo)對(duì)象必須要實(shí)現(xiàn)接口 * 2\. 代理對(duì)象,要實(shí)現(xiàn)與目標(biāo)對(duì)象一樣的接口 */class UserDaoProxy implements IUserDao{ // 代理對(duì)象,需要維護(hù)一個(gè)目標(biāo)對(duì)象 private IUserDao target = new UserDao(); @Override public void save() { System.out.println("代理操作: 開(kāi)啟事務(wù)..."); target.save(); // 執(zhí)行目標(biāo)對(duì)象的方法 System.out.println("代理操作:提交事務(wù)..."); } @Override public void find() { target.find(); }}
測(cè)試結(jié)果:
靜態(tài)代理雖然保證了業(yè)務(wù)類只需關(guān)注邏輯本身,代理對(duì)象的一個(gè)接口只服務(wù)于一種類型的對(duì)象。如果要代理的方法很多,勢(shì)必要為每一種方法都進(jìn)行代理。再者,如果增加一個(gè)方法,除了實(shí)現(xiàn)類需要實(shí)現(xiàn)這個(gè)方法外,所有的代理類也要實(shí)現(xiàn)此方法。增加了代碼的維護(hù)成本。那么要如何解決呢?答案是使用動(dòng)態(tài)代理。
3. 動(dòng)態(tài)代理原理及實(shí)踐
動(dòng)態(tài)代理模式:動(dòng)態(tài)代理類的源碼是在程序運(yùn)行期間,通過(guò) JVM 反射等機(jī)制動(dòng)態(tài)生成。代理類和委托類的關(guān)系是運(yùn)行時(shí)才確定的。實(shí)例如下:
package test.dynamicProxy; import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy; // 接口public interface IUserDao { void save(); void find();} //目標(biāo)對(duì)象class UserDao implements IUserDao{ @Override public void save() { System.out.println("模擬: 保存用戶!"); } @Override public void find() { System.out.println("查詢"); }} /** * 動(dòng)態(tài)代理: * 代理工廠,給多個(gè)目標(biāo)對(duì)象生成代理對(duì)象! * */class ProxyFactory { // 接收一個(gè)目標(biāo)對(duì)象 private Object target; public ProxyFactory(Object target) { this.target = target; } // 返回對(duì)目標(biāo)對(duì)象(target)代理后的對(duì)象(proxy) public Object getProxyInstance() { Object proxy = Proxy.newProxyInstance( target.getClass().getClassLoader(), // 目標(biāo)對(duì)象使用的類加載器 target.getClass().getInterfaces(), // 目標(biāo)對(duì)象實(shí)現(xiàn)的所有接口 new InvocationHandler() { // 執(zhí)行代理對(duì)象方法時(shí)候觸發(fā) @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 獲取當(dāng)前執(zhí)行的方法的方法名 String methodName = method.getName(); // 方法返回值 Object result = null; if ("find".equals(methodName)) { // 直接調(diào)用目標(biāo)對(duì)象方法 result = method.invoke(target, args); } else { System.out.println("開(kāi)啟事務(wù)..."); // 執(zhí)行目標(biāo)對(duì)象方法 result = method.invoke(target, args); System.out.println("提交事務(wù)..."); } return result; } } ); return proxy; }}
測(cè)試結(jié)果如下:
IUserDao proxy = (IUserDao)new ProxyFactory(target).getProxyInstance();
其實(shí)是 JDK 動(dòng)態(tài)生成了一個(gè)類去實(shí)現(xiàn)接口,隱藏了這個(gè)過(guò)程:
class $jdkProxy implements IUserDao{}
使用 JDK 生成的動(dòng)態(tài)代理的前提是目標(biāo)類必須有實(shí)現(xiàn)的接口。但這里又引入一個(gè)問(wèn)題,如果某個(gè)類沒(méi)有實(shí)現(xiàn)接口,就不能使用 JDK 動(dòng)態(tài)代理。所以 CGLIB 代理就是解決這個(gè)問(wèn)題的。
CGLIB 是以動(dòng)態(tài)生成的子類繼承目標(biāo)的方式實(shí)現(xiàn),在運(yùn)行期動(dòng)態(tài)的在內(nèi)存中構(gòu)建一個(gè)子類,如下:
CGLIB 使用的前提是目標(biāo)類不能為 final 修飾。因?yàn)?final 修飾的類不能被繼承。
現(xiàn)在,我們可以看看 AOP 的定義:面向切面編程,核心原理是使用動(dòng)態(tài)代理模式在方法執(zhí)行前后或出現(xiàn)異常時(shí)加入相關(guān)邏輯。
通過(guò)定義和前面代碼我們可以發(fā)現(xiàn)3點(diǎn):
AOP 是基于動(dòng)態(tài)代理模式。
AOP 是方法級(jí)別的。
AOP 可以分離業(yè)務(wù)代碼和關(guān)注點(diǎn)代碼(重復(fù)代碼),在執(zhí)行業(yè)務(wù)代碼時(shí),動(dòng)態(tài)的注入關(guān)注點(diǎn)代碼。切面就是關(guān)注點(diǎn)代碼形成的類。
4. Spring AOP
前文提到 JDK 代理和 CGLIB 代理兩種動(dòng)態(tài)代理。優(yōu)秀的 Spring 框架把兩種方式在底層都集成了進(jìn)去,我們無(wú)需擔(dān)心自己去實(shí)現(xiàn)動(dòng)態(tài)生成代理。那么,Spring是如何生成代理對(duì)象的?
創(chuàng)建容器對(duì)象的時(shí)候,根據(jù)切入點(diǎn)表達(dá)式攔截的類,生成代理對(duì)象。
如果目標(biāo)對(duì)象有實(shí)現(xiàn)接口,使用 JDK 代理。如果目標(biāo)對(duì)象沒(méi)有實(shí)現(xiàn)接口,則使用 CGLIB 代理。然后從容器獲取代理后的對(duì)象,在運(yùn)行期植入“切面”類的方法。通過(guò)查看 Spring 源碼,我們?cè)?DefaultAopProxyFactory 類中,找到這樣一段話。
簡(jiǎn)單的從字面意思看出:如果有接口,則使用 JDK 代理,反之使用 CGLIB ,這剛好印證了前文所闡述的內(nèi)容。Spring AOP 綜合兩種代理方式的使用前提有會(huì)如下結(jié)論:如果目標(biāo)類沒(méi)有實(shí)現(xiàn)接口,且 class 為 final 修飾的,則不能進(jìn)行 Spring AOP 編程!
知道了原理,現(xiàn)在我們將自己手動(dòng)實(shí)現(xiàn) Spring 的 AOP:
package test.spring_aop_anno; import org.aspectj.lang.ProceedingJoinPoint; public interface IUserDao { void save();} // 用于測(cè)試 CGLIB 動(dòng)態(tài)代理class OrderDao { public void save() { //int i =1/0; 用于測(cè)試異常通知 System.out.println("保存訂單..."); }} //用于測(cè)試 JDK 動(dòng)態(tài)代理class UserDao implements IUserDao { public void save() { //int i =1/0; 用于測(cè)試異常通知 System.out.println("保存用戶..."); }} //切面類class TransactionAop { public void beginTransaction() { System.out.println("[前置通知] 開(kāi)啟事務(wù).."); } public void commit() { System.out.println("[后置通知] 提交事務(wù).."); } public void afterReturing() { System.out.println("[返回后通知]"); } public void afterThrowing() { System.out.println("[異常通知]"); } public void arroud(ProceedingJoinPoint pjp) throws Throwable { System.out.println("[環(huán)繞前:]"); pjp.proceed(); // 執(zhí)行目標(biāo)方法 System.out.println("[環(huán)繞后:]"); }}
Spring 的 XML 配置文件:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- dao實(shí)例加入容器 --> <bean id="userDao" class="test.spring_aop_anno.UserDao"></bean> <!-- dao實(shí)例加入容器 --> <bean id="orderDao" class="test.spring_aop_anno.OrderDao"></bean> <!-- 實(shí)例化切面類 --> <bean id="transactionAop" class="test.spring_aop_anno.TransactionAop"></bean> <!-- Aop相關(guān)配置 --> <aop:config> <!-- 切入點(diǎn)表達(dá)式定義 --> <aop:pointcut expression="execution(* test.spring_aop_anno.*Dao.*(..))" id="transactionPointcut"/> <!-- 切面配置 --> <aop:aspect ref="transactionAop"> <!-- 【環(huán)繞通知】 --> <aop:around method="arroud" pointcut-ref="transactionPointcut"/> <!-- 【前置通知】 在目標(biāo)方法之前執(zhí)行 --> <aop:before method="beginTransaction" pointcut-ref="transactionPointcut" /> <!-- 【后置通知】 --> <aop:after method="commit" pointcut-ref="transactionPointcut"/> <!-- 【返回后通知】 --> <aop:after-returning method="afterReturing" pointcut-ref="transactionPointcut"/> <!-- 異常通知 --> <aop:after-throwing method="afterThrowing" pointcut-ref="transactionPointcut"/> </aop:aspect> </aop:config></beans>
切入點(diǎn)表達(dá)式不在這里介紹。參考 Spring AOP 切入點(diǎn)表達(dá)式
代碼的測(cè)試結(jié)果如下:
到這里,我們已經(jīng)全部介紹完Spring AOP?;氐介_(kāi)篇的問(wèn)題,我們拿它做什么?
Spring聲明式事務(wù)管理配置:請(qǐng)參考博主的另一篇文章:分布式系統(tǒng)架構(gòu)實(shí)戰(zhàn) demo:SSM+Dubbo
Controller層的參數(shù)校驗(yàn):參考 Spring AOP攔截Controller做參數(shù)校驗(yàn)
使用 Spring AOP 實(shí)現(xiàn) MySQL 數(shù)據(jù)庫(kù)讀寫(xiě)分離案例分析
在執(zhí)行方法前,判斷是否具有權(quán)限
對(duì)部分函數(shù)的調(diào)用進(jìn)行日志記錄:監(jiān)控部分重要函數(shù),若拋出指定的異常,可以以短信或郵件方式通知相關(guān)人員。
信息過(guò)濾,頁(yè)面轉(zhuǎn)發(fā)等等功能
博主一個(gè)人的力量有限,只能列舉這么多,歡迎評(píng)論區(qū)對(duì)文章做補(bǔ)充。
Spring AOP還能做什么,實(shí)現(xiàn)什么魔幻功能,就在于我們每一個(gè)平凡而又睿智的程序猿!
參考文章
Spring AOP 切入點(diǎn)表達(dá)式:http://blog.csdn.net/keda8997110/article/details/50747923
分布式系統(tǒng)架構(gòu)實(shí)戰(zhàn) demo:SSM+Dubbo:https://my.oschina.net/liughDevelop/blog/1480061
Spring AOP 攔截Controller做參數(shù)校驗(yàn):https://my.oschina.net/liughDevelop/blog/1480061
使用 Spring AOP 實(shí)現(xiàn) MySQL 數(shù)據(jù)庫(kù)讀寫(xiě)分離案例分析:http://blog.csdn.net/xlgen157387/article/details/53930382