AOP理解及底層原理

AOP 基礎(chǔ)概念

一、概述

AOP (Aspect Oriented Programming)

銀行系統(tǒng)的簡易取款流程如圖:

將方框里的流程合為一個(gè),另外系統(tǒng)還會(huì)有一個(gè)查詢余額流程,如圖:

這兩個(gè)業(yè)務(wù)有一個(gè)共同的驗(yàn)證流程,如圖:

為什么會(huì)有面向切面編程(AOP)?Java 是面向?qū)ο蟪绦蛟O(shè)計(jì)(OOP)的,但它有一些弊端。AOP 的真正目的是,業(yè)務(wù)開發(fā),事先只需考慮主流程,而忽略不重要的流程。AOP 可以把這個(gè)驗(yàn)證用戶的代碼提取出來,不放到主流程里去。比如當(dāng)要為多個(gè)不具有擔(dān)當(dāng)關(guān)系的工具引入一個(gè)公共舉動(dòng),例如日志、權(quán)限驗(yàn)證、事務(wù)等功能時(shí),只能在每個(gè)工具里引用公共舉動(dòng)。如果這樣做不便于維護(hù),并且還會(huì)有大量相同的代碼。AOP 面向方面編程基于 IOC,是對 OOP 的有益補(bǔ)充。AOP 是一種思想,不同的廠商或企業(yè)可能有不同的實(shí)現(xiàn)方式,為了更好的應(yīng)用 AOP 技術(shù),技術(shù)專家們成立了 AOP 聯(lián)盟來探討 AOP 的標(biāo)準(zhǔn)化。AOP 聯(lián)盟定義的 AOP 體系結(jié)構(gòu)把與 AOP 相關(guān)的概念大致分為由高到低、從使用到實(shí)現(xiàn)的三層關(guān)系, AOP 聯(lián)盟定義的 AOP 體系結(jié)構(gòu)如下圖:

在 AOP 聯(lián)盟定義的 AOP 體系結(jié)構(gòu)下有很多的實(shí)現(xiàn)者,例如:AspectJ、JBoss AOP、AspectWerkz、Spring AOP 等。

可以把上面業(yè)務(wù)方框當(dāng)塊板子,這塊板子插入一些控制流程,這些控制流程就可以當(dāng)成是 AOP 中的一個(gè)切面。所以 AOP 的本質(zhì)是在一系列縱向的業(yè)務(wù)流程中,把那些相同的子流程提取成一個(gè)橫向的面,如圖二,AOP 相當(dāng)于把相同的地方連一條橫線。

圖一
圖二

驗(yàn)證用戶這個(gè)控制流程就成了一個(gè)條線,也可以理解成一個(gè)切面。這里的切面只插了兩三個(gè)流程,如果其它流程也需要這個(gè)子流程,也可以插到其它地方去。

要想理解 Spring AOP,先理解代理模式。
代理模式的定義:為其他對象提供一種代理以控制對這個(gè)對象的訪問。在某些情況下,一個(gè)對象不適合或者不能直接引用另一個(gè)對象,而代理對象可以在客戶端和目標(biāo)對象之間起到中介的作用。比如 A 對象要做一件事情,在沒有代理前,自己來做;在對 A 代理后,由 A 的代理類 B 來做。代理其實(shí)是在原實(shí)例前后加了一層處理,這也是 AOP 的初級輪廓。

二、代理模式的原理及實(shí)踐

代理模式又分為靜態(tài)代理、動(dòng)態(tài)代理。

1??靜態(tài)代理原理及實(shí)踐
說白了,就是在程序運(yùn)行前就已經(jīng)存在代理類的字節(jié)碼文件,代理類和原始類的關(guān)系在運(yùn)行前就已經(jīng)確定。代碼如下:

接口:

public interface IUserDao {
    void save();
    void find();
}

目標(biāo)對象:

class UserDao implements IUserDao{
    @Override
    public void save() {
        System.out.println("模擬:保存用戶!");
    }
    @Override
    public void find() {
        System.out.println("模擬:查詢用戶");
    }
}

靜態(tài)代理特點(diǎn):
①目標(biāo)對象必須要實(shí)現(xiàn)接口
②代理對象,要實(shí)現(xiàn)與目標(biāo)對象一樣的接口

class UserDaoProxy implements IUserDao{
    // 代理對象,需要維護(hù)一個(gè)目標(biāo)對象
    private IUserDao target = new UserDao();
    @Override
    public void save() {
        System.out.println("代理操作: 開啟事務(wù)...");
        target.save(); // 執(zhí)行目標(biāo)對象的方法
        System.out.println("代理操作:提交事務(wù)...");
    }
    @Override
    public void find() {
        target.find();
    }
}

靜態(tài)代理保證了業(yè)務(wù)類只需要關(guān)注邏輯本身,代理對象的一個(gè)接口只需要服務(wù)于一種類型的對象。如果要代理的方法很多,勢必要為每一種方法都進(jìn)行代理。再者,如果增加一個(gè)方法,除了實(shí)現(xiàn)類需要實(shí)現(xiàn)這個(gè)方法外,所有的代理類也要實(shí)現(xiàn)此方法,增加了代碼的維護(hù)成本。那么要如何解決呢?答案是使用動(dòng)態(tài)代理。

2??動(dòng)態(tài)代理原理及實(shí)踐
動(dòng)態(tài)代理類的源碼是在程序運(yùn)行期間,通過 JVM 反射等機(jī)制動(dòng)態(tài)生成。代理類和委托類的關(guān)系是運(yùn)行時(shí)才確定的。實(shí)例如下:

接口:

public interface IUserDao {
    void save();
    void find();
}

目標(biāo)對象:

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)對象生成代理對象

class ProxyFactory {
    // 接收一個(gè)目標(biāo)對象
    private Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }
    // 返回對目標(biāo)對象(target)代理后的對象(proxy)
    public Object getProxyInstance() {
        Object proxy = Proxy.newProxyInstance(
                               // 目標(biāo)對象使用的類加載器
                target.getClass().getClassLoader(),
                               // 目標(biāo)對象實(shí)現(xiàn)的所有接口 
                target.getClass().getInterfaces(), 
                              // 執(zhí)行代理對象方法時(shí)候觸發(fā)
                new InvocationHandler() { 
                    @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)對象方法
                            result = method.invoke(target, args);
                        } else {
                            System.out.println("開啟事務(wù)...");
                            // 執(zhí)行目標(biāo)對象方法
                            result = method.invoke(target, args);
                            System.out.println("提交事務(wù)...");
                        }
                        return result;
                    }
                }
                );
        return proxy;
    }
}
IUserDao proxy = (IUserDao)new ProxyFactory(target).getProxyInstance();

其實(shí)是 JDK 動(dòng)態(tài)生成了一個(gè)類去實(shí)現(xiàn)接口,隱藏了這個(gè)過程:

class $jdkProxy implements IUserDao{}

使用 JDK 生成動(dòng)態(tài)代理的前提是目標(biāo)類必須有實(shí)現(xiàn)的接口。如果某個(gè)類沒有實(shí)現(xiàn)接口,就不能使用 JDK 動(dòng)態(tài)代理。CGLIB 代理可以解決這個(gè)問題。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)邏輯。

通過定義和前面代碼可以發(fā)現(xiàn)3點(diǎn):

  1. AOP 是基于動(dòng)態(tài)代理模式。
  2. AOP 是方法級別的。
  3. AOP 可以分離業(yè)務(wù)代碼和關(guān)注點(diǎn)代碼(重復(fù)代碼)。在執(zhí)行業(yè)務(wù)代碼時(shí),動(dòng)態(tài)的注入關(guān)注點(diǎn)代碼。切面就是關(guān)注點(diǎn)代碼形成的類。

三、Spring AOP原理及實(shí)戰(zhàn)

Spring 框架把 JDK 代理和 CGLIB 代理兩種動(dòng)態(tài)代理在底層都集成了進(jìn)去,開發(fā)者無需自己去實(shí)現(xiàn)動(dòng)態(tài)生成代理。那么,Spring 是如何生成代理對象的?

  1. 創(chuàng)建容器對象的時(shí)候,根據(jù)切入點(diǎn)表達(dá)式攔截的類,生成代理對象。
  2. 如果目標(biāo)類有實(shí)現(xiàn)接口,使用 JDK 代理;如果目標(biāo)類沒有實(shí)現(xiàn)接口,且 class 不為 final 修飾的,則使用 CGLIB 代理;否則不能進(jìn)行 Spring AOP 編程。然后從容器獲取代理后的對象,在運(yùn)行期植入“切面”類的方法。通過查看 Spring 源碼,在 DefaultAopProxyFactory 類中,找到這樣一段話。
DefaultAopProxyFactory

手動(dòng)實(shí)現(xiàn) Spring 的 AOP:
接口:

public interface IUserDao {
    void save();
}

用于測試 CGLIB 動(dòng)態(tài)代理:

class OrderDao {
    public void save() {
        //int i =1/0; 用于測試異常通知
        System.out.println("保存訂單...");
    }
}

用于測試 JDK 動(dòng)態(tài)代理:

class UserDao implements IUserDao {
    public void save() {
        //int i =1/0; 用于測試異常通知
        System.out.println("保存用戶...");
    }
}

切面類:

class TransactionAop {
    public void beginTransaction() {
        System.out.println("[前置通知] 開啟事務(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>

四、Spring AOP 應(yīng)用場景

  1. Spring 聲明式事務(wù)管理配置;
  2. Controller 層的參數(shù)校驗(yàn);
  3. 使用 Spring AOP 實(shí)現(xiàn) MySQL 數(shù)據(jù)庫讀寫分離案例分析;
  4. 在執(zhí)行方法前,判斷是否具有權(quán)限,Authentication 權(quán)限檢查;
  5. 對部分函數(shù)的調(diào)用進(jìn)行日志記錄:監(jiān)控部分重要函數(shù),若拋出指定的異常,可以以短信或郵件方式通知相關(guān)人員;
  6. 信息過濾,頁面轉(zhuǎn)發(fā)等等功能。
    .
    .
    .
    Caching 緩存
    Context passing 內(nèi)容傳遞
    Error handling 錯(cuò)誤處理
    Lazy loading 延遲加載
    Debugging  調(diào)試
    logging, tracing, profiling and monitoring 日志記錄,跟蹤,優(yōu)化,校準(zhǔn)
    Performance optimization 性能優(yōu)化,效率檢查
    Persistence  持久化
    Resource pooling 資源池
    Synchronization 同步
    Transactions 事務(wù)管理
    另外 Filter 的實(shí)現(xiàn)和 struts2 的攔截器的實(shí)現(xiàn)都是 AOP 思想的體現(xiàn)。

-------------------------------------------------------------

Spring 不嘗試提供最為完善的 AOP 實(shí)現(xiàn),它更側(cè)重于提供一種和 Spring IOC 容器整個(gè)的 AOP 實(shí)現(xiàn),用于解決實(shí)際的問題,在 Spring 中無縫的整合了 Spring AOP、Spring IOC 和 AspectJ。在使用 Spring AOP 的時(shí)候只是簡單的配置一下(通過 XML 或注解進(jìn)行配置),Spring AOP 在內(nèi)部 ProxyFactory 來創(chuàng)建代理對象,然后調(diào)用目標(biāo)方法。

動(dòng)態(tài)代理或者設(shè)計(jì)模式很重要!Spring AOP 用到了動(dòng)態(tài)代理,Spring 事務(wù)管理用到了動(dòng)態(tài)代理,MyBatis 數(shù)據(jù)庫連接池用到了動(dòng)態(tài)代理,MyBatis 創(chuàng)建 Mapper 用到了動(dòng)態(tài)代理等等!

  1. AOP 面向方面編程基于 IOC,是對 OOP 的有益補(bǔ)充。

  2. AOP 利用一種稱為“橫切”的技術(shù),剖解開封裝的對象內(nèi)部,并將那些影響了多個(gè)類的公共行為封裝到一個(gè)可重用模塊,并將其名為“Aspect”,即方面。所謂“方面”,簡單地說,就是將那些與業(yè)務(wù)無關(guān),卻為業(yè)務(wù)模塊所共同調(diào)用的邏輯或責(zé)任封裝起來,比如日志記錄,便于減少系統(tǒng)的重復(fù)代碼,降低模塊間的耦合度,并有利于未來的可操作性和可維護(hù)性。

  3. AOP 代表的是一個(gè)橫向的關(guān)系,將“對象”比作一個(gè)空心的圓柱體,其中封裝的是對象的屬性和行為;則面向方面編程的方法,就是將這個(gè)圓柱體以切面形式剖開,選擇性的提供業(yè)務(wù)邏輯。而剖開的切面,也就是所謂的“方面”了。然后它又以巧奪天工的妙手將這些剖開的切面復(fù)原,不留痕跡,但完成了效果。

  4. 實(shí)現(xiàn) AOP 的技術(shù),主要分為兩大類:一是采用動(dòng)態(tài)代理技術(shù),利用截取消息的方式,對該消息進(jìn)行裝飾,以取代原有對象行為的執(zhí)行;二是采用靜態(tài)織入的方式,引入特定的語法創(chuàng)建“方面”,從而使得編譯器可以在編譯期間織入有關(guān)“方面”的代碼。

  5. Spring 實(shí)現(xiàn) AOP:JDK 動(dòng)態(tài)代理和 CGLIB 代理。JDK 動(dòng)態(tài)代理:其代理對象必須是某個(gè)接口的實(shí)現(xiàn),它是通過在運(yùn)行期間創(chuàng)建一個(gè)接口的實(shí)現(xiàn)類來完成對目標(biāo)對象的代理;其核心的兩個(gè)類是 InvocationHandler 和 Proxy。CGLIB 代理:實(shí)現(xiàn)原理類似于 JDK 動(dòng)態(tài)代理,只是它在運(yùn)行期間生成的代理對象是針對目標(biāo)類擴(kuò)展的子類。CGLIB 是高效的代碼生成包,底層是依靠 ASM(開源的 Java 字節(jié)碼編輯類庫)操作字節(jié)碼實(shí)現(xiàn)的,性能比 JDK 強(qiáng);需要引入包 asm.jar 和 cglib.jar。使用 AspectJ 注入式切面和 @AspectJ 注解驅(qū)動(dòng)的切面實(shí)際上底層也是通過動(dòng)態(tài)代理實(shí)現(xiàn)的。

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

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

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