Spring AOP是什么?你都拿它做什么?

轉(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)行討論:

  1. 代理模式

  2. 靜態(tài)代理原理及實(shí)踐

  3. 動(dòng)態(tài)代理原理及實(shí)踐

  4. 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é)果:

image

靜態(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é)果如下:

image
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ì)象的?

  1. 創(chuàng)建容器對(duì)象的時(shí)候,根據(jù)切入點(diǎn)表達(dá)式攔截的類,生成代理對(duì)象。

  2. 如果目標(biāo)對(duì)象有實(shí)現(xiàn)接口,使用 JDK 代理。如果目標(biāo)對(duì)象沒(méi)有實(shí)現(xiàn)接口,則使用 CGLIB 代理。然后從容器獲取代理后的對(duì)象,在運(yùn)行期植入“切面”類的方法。通過(guò)查看 Spring 源碼,我們?cè)?DefaultAopProxyFactory 類中,找到這樣一段話。

image

簡(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é)果如下:

image

到這里,我們已經(jīng)全部介紹完Spring AOP?;氐介_(kāi)篇的問(wèn)題,我們拿它做什么?

  1. Spring聲明式事務(wù)管理配置:請(qǐng)參考博主的另一篇文章:分布式系統(tǒng)架構(gòu)實(shí)戰(zhàn) demo:SSM+Dubbo

  2. Controller層的參數(shù)校驗(yàn):參考 Spring AOP攔截Controller做參數(shù)校驗(yàn)

  3. 使用 Spring AOP 實(shí)現(xiàn) MySQL 數(shù)據(jù)庫(kù)讀寫(xiě)分離案例分析

  4. 在執(zhí)行方法前,判斷是否具有權(quán)限

  5. 對(duì)部分函數(shù)的調(diào)用進(jìn)行日志記錄:監(jiān)控部分重要函數(shù),若拋出指定的異常,可以以短信或郵件方式通知相關(guān)人員。

  6. 信息過(guò)濾,頁(yè)面轉(zhuǎn)發(fā)等等功能

博主一個(gè)人的力量有限,只能列舉這么多,歡迎評(píng)論區(qū)對(duì)文章做補(bǔ)充。

Spring AOP還能做什么,實(shí)現(xiàn)什么魔幻功能,就在于我們每一個(gè)平凡而又睿智的程序猿!

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容