Spring AOP

一就珠、 AOP 簡介

1.1 什么是 AOP

AOP (Aspect Orient Programming),直譯過來就是 面向切面編程革为。AOP 是一種編程思想,是面向?qū)ο缶幊蹋∣OP)的一種補(bǔ)充。面向?qū)ο缶幊虒⒊绦虺橄蟪筛鱾€層次的對象播瞳,而面向切面編程是將程序抽象成各個切面。

1.2 為什么需要 AOP

OOP引入封裝免糕、繼承和多態(tài)性等概念來建立一種對象層次結(jié)構(gòu)赢乓,用以模擬公共行為的一個集合。當(dāng)我們需要為分散的對象引入公共行為的時候石窑,OOP則顯得無能為力牌芋。通過案例,感受一下:
A類:

public class A {
    public void executeA(){
        //其他業(yè)務(wù)操作省略......
        recordLog();
    }

    public void recordLog(){
        //....記錄日志并上報(bào)日志系統(tǒng)
    }
}

B類:

public class B {
    public void executeB(){
        //其他業(yè)務(wù)操作省略......
        recordLog();
    }

    public void recordLog(){
        //....記錄日志并上報(bào)日志系統(tǒng)
    }
}

C類:

public class C {
    public void executeC(){
        //其他業(yè)務(wù)操作省略......
        recordLog();
    }

    public void recordLog(){
        //....記錄日志并上報(bào)日志系統(tǒng)
    }
}

假設(shè)存在A松逊、B姜贡、C三個類,需要對它們的方法訪問進(jìn)行日志記錄棺棵,在代碼中各種存在recordLog方法進(jìn)行日志記錄并上報(bào)楼咳,或許對現(xiàn)在的工程師來說幾乎不可能寫出如此糟糕的代碼熄捍,但在OOP這樣的寫法是允許的,而且在OOP開始階段這樣的代碼確實(shí)并大量存在著母怜,直到工程師實(shí)在忍受不了一次修改余耽,到處挖墳時(修改recordLog內(nèi)容),才下定決心解決該問題苹熏,為了解決程序間過多冗余代碼的問題碟贾,工程師便開始使用下面的編碼方式:

//A類
public class A {
    public void executeA(){
        //其他業(yè)務(wù)操作省略...... args 參數(shù),一般會傳遞類名轨域,方法名稱 或信息(這樣的信息一般不輕易改動)
        Report.recordLog(args ...);
    }
}

//B類
public class B {
    public void executeB(){
        //其他業(yè)務(wù)操作省略......
        Report.recordLog(args ...);
    }
}

//C類
public class C {
    public void executeC(){
        //其他業(yè)務(wù)操作省略......
        Report.recordLog(args ...);
    }
}

//record
public class Report {
    public static void recordLog(args ...){
        //....記錄日志并上報(bào)日志系統(tǒng)
    }
}

這樣操作后袱耽,我們欣喜地發(fā)現(xiàn)問題似乎得到了解決,當(dāng)上報(bào)信息內(nèi)部方法需要調(diào)整時干发,只需調(diào)整Report類中recordLog方法體朱巨,也就避免了隨處挖墳的問題,大大降低了軟件后期維護(hù)的復(fù)雜度枉长。確實(shí)如此冀续,而且除了上述的解決方案,還存在一種通過繼承來解決的方式必峰,采用這種方式洪唐,只需把相通的代碼放到一個類(一般是其他類的父類)中,其他的類(子類)通過繼承父類獲取相通的代碼吼蚁,如下:

//通用父類
public class Dparent {
    public void commond(){
        //通用代碼
    }
}
//A 繼承 Dparent 
public class A extends Dparent {
    public void executeA(){
        //其他業(yè)務(wù)操作省略......
        commond();
    }
}
//B 繼承 Dparent 
public class B extends Dparent{
    public void executeB(){
        //其他業(yè)務(wù)操作省略......
        commond();
    }
}
//C 繼承 Dparent 
public class C extends Dparent{
    public void executeC(){
        //其他業(yè)務(wù)操作省略......
        commond();
    }
}

顯然代碼冗余也得到了解決凭需,這種通過繼承抽取通用代碼的方式也稱為縱向拓展,與之對應(yīng)的還有橫向拓展肝匆。事實(shí)上有了上述兩種解決方案后功炮,在大部分業(yè)務(wù)場景的代碼冗余問題也得到了實(shí)實(shí)在在的解決。
但是隨著軟件開發(fā)的系統(tǒng)越來越復(fù)雜术唬,工程師認(rèn)識到薪伏,傳統(tǒng)的OOP程序經(jīng)常表現(xiàn)出一些不自然的現(xiàn)象,核心業(yè)務(wù)中總摻雜著一些不相關(guān)聯(lián)的特殊業(yè)務(wù)粗仓,如日志記錄嫁怀,權(quán)限驗(yàn)證,事務(wù)控制借浊,性能檢測塘淑,錯誤信息檢測等等,這些特殊業(yè)務(wù)可以說和核心業(yè)務(wù)沒有根本上的關(guān)聯(lián)而且核心業(yè)務(wù)也不關(guān)心它們蚂斤,比如在用戶管理模塊中存捺,該模塊本身只關(guān)心與用戶相關(guān)的業(yè)務(wù)信息處理,至于其他的業(yè)務(wù)完全可以不理會,我們看一個簡單例子協(xié)助理解這個問題

public interface IUserService {

    void saveUser();

    void deleteUser();

    void findAllUser();
}
//實(shí)現(xiàn)類
public class UserServiceImpl implements IUserService {

    //核心數(shù)據(jù)成員

    //日志操作對象

    //權(quán)限管理對象

    //事務(wù)控制對象

    @Override
    public void saveUser() {

        //權(quán)限驗(yàn)證(假設(shè)權(quán)限驗(yàn)證丟在這里)

        //事務(wù)控制

        //日志操作

        //進(jìn)行Dao層操作
        userDao.saveUser();

    }

    @Override
    public void deleteUser() {

    }

    @Override
    public void findAllUser() {

    }
}

上述代碼中我們注意到一些問題捌治,權(quán)限岗钩,日志,事務(wù)都不是用戶管理的核心業(yè)務(wù)肖油,也就是說用戶管理模塊除了要處理自身的核心業(yè)務(wù)外兼吓,還需要處理權(quán)限,日志森枪,事務(wù)等待這些雜七雜八的不相干業(yè)務(wù)的外圍操作视搏,而且這些外圍操作同樣會在其他業(yè)務(wù)模塊中出現(xiàn),這樣就會造成如下問題

  • 代碼混亂:核心業(yè)務(wù)模塊可能需要兼顧處理其他不相干的業(yè)務(wù)外圍操作县袱,這些外圍操作可能會混亂核心操作的代碼浑娜,而且當(dāng)外圍模塊有重大修改時也會影響到核心模塊,這顯然是不合理的式散。
  • 代碼分散和冗余:同樣的功能代碼筋遭,在其他的模塊幾乎隨處可見,導(dǎo)致代碼分散并且冗余度高杂数。
  • 代碼質(zhì)量低擴(kuò)展難:由于不太相關(guān)的業(yè)務(wù)代碼混雜在一起宛畦,無法專注核心業(yè)務(wù)代碼瘸洛,當(dāng)進(jìn)行類似無關(guān)業(yè)務(wù)擴(kuò)展時又會直接涉及到核心業(yè)務(wù)的代碼揍移,導(dǎo)致拓展性低。
    顯然前面分析的兩種解決方案已束手無策了反肋,那么該如何解決呢那伐?事實(shí)上我們知道諸如日志,權(quán)限石蔗,事務(wù)罕邀,性能監(jiān)測等業(yè)務(wù)幾乎涉及到了所有的核心模塊,如果把這些特殊的業(yè)務(wù)代碼直接到核心業(yè)務(wù)模塊的代碼中就會造成上述的問題养距,而工程師更希望的是這些模塊可以實(shí)現(xiàn)熱插拔特性而且無需把外圍的代碼入侵到核心模塊中诉探,這樣在日后的維護(hù)和擴(kuò)展也將會有更佳的表現(xiàn),假設(shè)現(xiàn)在我們把日志棍厌、權(quán)限肾胯、事務(wù)、性能監(jiān)測等外圍業(yè)務(wù)看作單獨(dú)的關(guān)注點(diǎn)(也可以理解為單獨(dú)的模塊)耘纱,每個關(guān)注點(diǎn)都可以在需要它們的時刻及時被運(yùn)用而且無需提前整合到核心模塊中敬肚,這種形式相當(dāng)下圖:


    20170215092953013.png

從圖可以看出,每個關(guān)注點(diǎn)與核心業(yè)務(wù)模塊分離束析,作為單獨(dú)的功能艳馒,橫切幾個核心業(yè)務(wù)模塊,這樣的做的好處是顯而易見的员寇,每份功能代碼不再單獨(dú)入侵到核心業(yè)務(wù)類的代碼中弄慰,即核心模塊只需關(guān)注自己相關(guān)的業(yè)務(wù)第美,當(dāng)需要外圍業(yè)務(wù)(日志,權(quán)限曹动,性能監(jiān)測斋日、事務(wù)控制)時,這些外圍業(yè)務(wù)會通過一種特殊的技術(shù)自動應(yīng)用到核心模塊中墓陈,這些關(guān)注點(diǎn)有個特殊的名稱恶守,叫做“橫切關(guān)注點(diǎn)”,上圖也很好的表現(xiàn)出這個概念贡必,另外這種抽象級別的技術(shù)就叫AOP(面向切面編程)兔港,正如上圖所展示的橫切核心模塊的整面,因此AOP的概念就出現(xiàn)了仔拟,而所謂的特殊技術(shù)也就面向切面編程的實(shí)現(xiàn)技術(shù)衫樊,AOP的實(shí)現(xiàn)技術(shù)有多種,其中與Java無縫對接的是一種稱為AspectJ的技術(shù)利花。那么這種切面技術(shù)(AspectJ)是如何在Java中的應(yīng)用呢科侈?不必?fù)?dān)心,也不必全面了解AspectJ炒事,對于AspectJ臀栈,我們只會進(jìn)行簡單的了解,從而為理解Spring中的AOP打下良好的基礎(chǔ)(Spring AOP 與AspectJ 實(shí)現(xiàn)原理上并不完全一致挠乳,但功能上是相似的)权薯,畢竟Spring中已實(shí)現(xiàn)AOP主要功能,開發(fā)中直接使用Spring中提供的AOP功能即可睡扬,除非我們想單獨(dú)使用AspectJ的其他功能盟蚣。這里還需要注意的是,AOP的出現(xiàn)確實(shí)解決外圍業(yè)務(wù)代碼與核心業(yè)務(wù)代碼分離的問題卖怜,但它并不會替代OOP屎开,如果說OOP的出現(xiàn)是把編碼問題進(jìn)行模塊化,那么AOP就是把涉及到眾多模塊的某一類問題進(jìn)行統(tǒng)一管理马靠,因此在實(shí)際開發(fā)中AOP和OOP同時存在并不奇怪奄抽,后面將會慢慢體會帶這點(diǎn),好的虑粥,讓我們開始AspectJ吧如孝。

二、AspectJ-AOP的領(lǐng)跑者

2.1環(huán)境搭建

首先通過maven倉庫下載工具包aspectjtools-1.8.9.jar娩贷,該工具包包含ajc核心編譯器第晰,然后打開idea檢查是否已安裝aspectJ的插件:
20201014155354.png

配置項(xiàng)目使用ajc編譯器(替換javac)如下圖:
20201014155710.png

如果使用maven開發(fā)(否則在libs目錄自行引入jar)則在pom文件中添加aspectJ的核心依賴包,包含了AspectJ運(yùn)行時的核心庫文件:

<dependency>
    <groupId>aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.5.4</version>
</dependency>

新建文件處創(chuàng)建aspectJ文件,然后就可以像運(yùn)行java文件一樣茁瘦,操作aspect文件了品抽。
20201014161547.png

2.2簡單示例

編寫一個HelloWord的類,然后利用AspectJ技術(shù)切入該類的執(zhí)行過程甜熔。

public class HelloWorld {
    public void sayHello(){
        System.out.println("hello world!");
    }

    public static void main(String[] args) {
        HelloWorld helloWorld = new HelloWorld();
        helloWorld.sayHello();
    }
}

編寫AspectJ類圆恤,注意關(guān)鍵字為aspect(MyAspectJDemo.aj,其中aj為AspectJ的后綴),含義與class相同腔稀,即定義一個AspectJ的類

public aspect MyAspectJDemo {
    /**
     *  定義切點(diǎn),日志記錄切點(diǎn)
     */
    pointcut recordLog():call(* HelloWorld.sayHello(..));

    /**
     *  定義切點(diǎn),權(quán)限驗(yàn)證
     */
    pointcut authCheck():call(* HelloWorld.sayHello(..));

    /**
     *  定義前置通知
     */
    before():authCheck(){
        System.out.println("sayHello方法執(zhí)行前驗(yàn)證權(quán)限");
    }

    /**
     *  定義后置通知
     */
    after():recordLog(){
        System.out.println("sayHello方法執(zhí)行后記錄日志");
    }
}

運(yùn)行helloworld的main函數(shù):
20201014162815.png

我們發(fā)現(xiàn)盆昙,明明只運(yùn)行了main函數(shù),卻在sayHello函數(shù)運(yùn)行前后分別進(jìn)行了權(quán)限驗(yàn)證和日志記錄焊虏,事實(shí)上這就是AspectJ的功勞了淡喜。對aspectJ有了感性的認(rèn)識后,再來聊聊aspectJ到底是什么诵闭?AspectJ是一個java實(shí)現(xiàn)的AOP框架炼团,它能夠?qū)ava代碼進(jìn)行AOP編譯(一般在編譯期進(jìn)行),讓java代碼具有AspectJ的AOP功能(當(dāng)然需要特殊的編譯器)疏尿,可以這樣說AspectJ是目前實(shí)現(xiàn)AOP框架中最成熟瘟芝,功能最豐富的語言,更幸運(yùn)的是褥琐,AspectJ與java程序完全兼容锌俱,幾乎是無縫關(guān)聯(lián),因此對于有java編程基礎(chǔ)的工程師踩衩,上手和使用都非常容易嚼鹉。在案例中贩汉,我們使用aspect關(guān)鍵字定義了一個類驱富,這個類就是一個切面,它可以是單獨(dú)的日志切面(功能)匹舞,也可以是權(quán)限切面或者其他褐鸥,在切面內(nèi)部使用了pointcut定義了兩個切點(diǎn),一個用于權(quán)限驗(yàn)證赐稽,一個用于日志記錄叫榕,而所謂的切點(diǎn)就是那些需要應(yīng)用切面的方法,如需要在sayHello方法執(zhí)行前后進(jìn)行權(quán)限驗(yàn)證和日志記錄姊舵,那么就需要捕捉該方法晰绎,而pointcut就是定義這些需要捕捉的方法(常常是不止一個方法的),這些方法也稱為目標(biāo)方法括丁,最后還定義了兩個通知荞下,通知就是那些需要在目標(biāo)方法前后執(zhí)行的函數(shù),如before()即前置通知在目標(biāo)方法之前執(zhí)行,即在sayHello()方法執(zhí)行前進(jìn)行權(quán)限驗(yàn)證尖昏,另一個是after()即后置通知仰税,在sayHello()之后執(zhí)行,如進(jìn)行日志記錄抽诉。到這里也就可以確定陨簇,切面就是切點(diǎn)和通知的組合體,組成一個單獨(dú)的結(jié)構(gòu)供后續(xù)使用
簡單說明一下切點(diǎn)的定義語法:關(guān)鍵字為pointcut迹淌,定義切點(diǎn)河绽,后面跟著函數(shù)名稱,最后編寫匹配表達(dá)式唉窃,此時函數(shù)一般使用call()或者execution()進(jìn)行匹配葵姥,這里我們統(tǒng)一使用call()

pointcut 函數(shù)名 : 匹配表達(dá)式

關(guān)于定義通知的語法:首先通知有5種類型分別如下:

  • before 目標(biāo)方法執(zhí)行前執(zhí)行,前置通知
  • after 目標(biāo)方法執(zhí)行后執(zhí)行句携,后置通知
  • after returning 目標(biāo)方法返回時執(zhí)行 榔幸,后置返回通知
  • after throwing 目標(biāo)方法拋出異常時執(zhí)行 異常通知
  • around 在目標(biāo)函數(shù)執(zhí)行中執(zhí)行,可控制目標(biāo)函數(shù)是否執(zhí)行矮嫉,環(huán)繞通知
    語法:
[返回值類型] 通知函數(shù)名稱(參數(shù)) [returning/throwing 表達(dá)式]:連接點(diǎn)函數(shù)(切點(diǎn)函數(shù)){ 
函數(shù)體 
}

案例如下削咆,其中要注意around通知即環(huán)繞通知,可以通過proceed()方法控制目標(biāo)函數(shù)是否執(zhí)行蠢笋。

/**
  * 定義前置通知
  *
  * before(參數(shù)):連接點(diǎn)函數(shù){
  *     函數(shù)體
  * }
  */
 before():authCheck(){
     System.out.println("sayHello方法執(zhí)行前驗(yàn)證權(quán)限");
 }

 /**
  * 定義后置通知
  * after(參數(shù)):連接點(diǎn)函數(shù){
  *     函數(shù)體
  * }
  */
 after():recordLog(){
     System.out.println("sayHello方法執(zhí)行后記錄日志");
 }


 /**
  * 定義后置通知帶返回值
  * after(參數(shù))returning(返回值類型):連接點(diǎn)函數(shù){
  *     函數(shù)體
  * }
  */
 after()returning(int x): get(){
     System.out.println("返回值為:"+x);
 }

 /**
  * 異常通知
  * after(參數(shù)) throwing(返回值類型):連接點(diǎn)函數(shù){
  *     函數(shù)體
  * }
  */
 after() throwing(Exception e):sayHello2(){
     System.out.println("拋出異常:"+e.toString());
 }



 /**
  * 環(huán)繞通知 可通過proceed()控制目標(biāo)函數(shù)是否執(zhí)行
  * Object around(參數(shù)):連接點(diǎn)函數(shù){
  *     函數(shù)體
  *     Object result=proceed();//執(zhí)行目標(biāo)函數(shù)
  *     return result;
  * }
  */
 Object around():aroundAdvice(){
     System.out.println("sayAround 執(zhí)行前執(zhí)行");
     Object result=proceed();//執(zhí)行目標(biāo)函數(shù)
     System.out.println("sayAround 執(zhí)行后執(zhí)行");
     return result;
 }

切入點(diǎn)(pointcut)和通知(advice)的概念已比較清晰拨齐,而切面則是定義切入點(diǎn)和通知的組合如上述使用aspect關(guān)鍵字定義的MyAspectJDemo,把切面應(yīng)用到目標(biāo)函數(shù)的過程稱為織入(weaving)昨寞。在前面定義的HelloWord類中除了sayHello函數(shù)外瞻惋,還有main函數(shù)篮洁,以后可能還會定義其他函數(shù)蔑鹦,而這些函數(shù)都可以稱為目標(biāo)函數(shù),也就是說這些函數(shù)執(zhí)行前后也都可以切入通知的代碼芥炭,這些目標(biāo)函數(shù)統(tǒng)稱為連接點(diǎn)享怀,切入點(diǎn)(pointcut)的定義正是從這些連接點(diǎn)中過濾出來的羽峰,下圖協(xié)助理解。
20170216232225542.png

2.3 AspectJ的織入方式及其原理概要

經(jīng)過前面的簡單介紹添瓷,我們已初步掌握了AspectJ的一些語法和概念梅屉,但這樣仍然是不夠的,我們?nèi)孕枰私釧spectJ應(yīng)用到j(luò)ava代碼的過程(這個過程稱為織入)鳞贷,對于織入這個概念坯汤,可以簡單理解為aspect(切面)應(yīng)用到目標(biāo)函數(shù)(類)的過程。對于這個過程搀愧,一般分為動態(tài)織入和靜態(tài)織入惰聂,動態(tài)織入的方式是在運(yùn)行時動態(tài)將要增強(qiáng)的代碼織入到目標(biāo)類中凿滤,這樣往往是通過動態(tài)代理技術(shù)完成的,如Java JDK的動態(tài)代理(Proxy庶近,底層通過反射實(shí)現(xiàn))或者CGLIB的動態(tài)代理(底層通過繼承實(shí)現(xiàn))翁脆,Spring AOP采用的就是基于運(yùn)行時增強(qiáng)的代理技術(shù),這點(diǎn)后面會分析鼻种,這里主要重點(diǎn)分析一下靜態(tài)織入反番,ApectJ采用的就是靜態(tài)織入的方式。ApectJ主要采用的是編譯期織入叉钥,在這個期間使用AspectJ的acj編譯器(類似javac)把a(bǔ)spect類編譯成class字節(jié)碼后罢缸,在java目標(biāo)類編譯時織入,即先編譯aspect類再編譯目標(biāo)類投队。
20170219102612181.png

關(guān)于ajc編譯器枫疆,是一種能夠識別aspect語法的編譯器,它是采用java語言編寫的敷鸦,由于javac并不能識別aspect語法息楔,便有了ajc編譯器,注意ajc編譯器也可編譯java文件扒披。為了更直觀了解aspect的織入方式值依,我們打開前面案例中已編譯完成的HelloWord.class文件,反編譯后的java代碼如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.gaara.aspectJ;

import org.aspectj.lang.NoAspectBoundException;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class MyAspectJDemo {
    static {
        try {
            ajc$postClinit();
        } catch (Throwable var1) {
            ajc$initFailureCause = var1;
        }

    }

    public MyAspectJDemo() {
    }

    @Before(
        value = "authCheck()",
        argNames = ""
    )
    public void ajc$before$com_gaara_aspectJ_MyAspectJDemo$1$22c5541() {
        System.out.println("sayHello方法執(zhí)行前驗(yàn)證權(quán)限");
    }

    @After(
        value = "recordLog()",
        argNames = ""
    )
    public void ajc$after$com_gaara_aspectJ_MyAspectJDemo$2$4d789574() {
        System.out.println("sayHello方法執(zhí)行后記錄日志");
    }

    public static MyAspectJDemo aspectOf() {
        if (ajc$perSingletonInstance == null) {
            throw new NoAspectBoundException("com_gaara_aspectJ_MyAspectJDemo", ajc$initFailureCause);
        } else {
            return ajc$perSingletonInstance;
        }
    }

    public static boolean hasAspect() {
        return ajc$perSingletonInstance != null;
    }
}

AspectJ的織入原理已很明朗了碟案,當(dāng)然除了編譯期織入愿险,還存在鏈接期(編譯后)織入,即將aspect類和java目標(biāo)類同時編譯成字節(jié)碼文件后价说,再進(jìn)行織入處理辆亏,這種方式比較有助于已編譯好的第三方j(luò)ar和Class文件進(jìn)行織入操作,掌握以上AspectJ知識點(diǎn)就足以協(xié)助理解Spring AOP了鳖目。

三扮叨、Spring AOP

3.1 簡單示例

Spring AOP 與ApectJ 的目的一致,都是為了統(tǒng)一處理橫切業(yè)務(wù)疑苔,但與AspectJ不同的是甫匹,Spring AOP 并不嘗試提供完整的AOP功能(即使它完全可以實(shí)現(xiàn))甸鸟,Spring AOP 更注重的是與Spring IOC容器的結(jié)合惦费,并結(jié)合該優(yōu)勢來解決橫切業(yè)務(wù)的問題,因此在AOP的功能完善方面抢韭,相對來說AspectJ具有更大的優(yōu)勢薪贫,同時,Spring注意到AspectJ在AOP的實(shí)現(xiàn)方式上依賴于特殊編譯器(ajc編譯器),因此Spring很機(jī)智回避了這點(diǎn)刻恭,轉(zhuǎn)向采用動態(tài)代理技術(shù)的實(shí)現(xiàn)原理來構(gòu)建Spring AOP的內(nèi)部機(jī)制(動態(tài)織入)瞧省,這是與AspectJ(靜態(tài)織入)最根本的區(qū)別扯夭。在AspectJ 1.5后,引入@Aspect形式的注解風(fēng)格的開發(fā)鞍匾,Spring也非辰幌矗快地跟進(jìn)了這種方式,因此Spring 2.0后便使用了與AspectJ一樣的注解橡淑。請注意构拳,Spring 只是使用了與 AspectJ 5 一樣的注解,但仍然沒有使用 AspectJ 的編譯器梁棠,底層依是動態(tài)代理技術(shù)的實(shí)現(xiàn)置森,因此并不依賴于 AspectJ 的編譯器。下面我們先通過一個簡單的案例來演示Spring AOP的入門程序
接口類:

public interface UserDao {
    int addUser();

    void updateUser();

    void deleteUser();

    void findUser();
}

實(shí)現(xiàn)類:

@Repository
public class UserDaoImpl implements UserDao{
    public int addUser() {
        System.out.println("add user ......");
        return 6666;
    }

    public void updateUser() {
        System.out.println("update user ......");
    }

    public void deleteUser() {
        System.out.println("delete user ......");
    }

    public void findUser() {
        System.out.println("find user ......");
    }
}

aspect類:

@Aspect
public class MyAspect {

    /**
     * 前置通知
     */
    @Before("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
    public void  before(){
        System.out.println("前置通知....");
    }

    /**
     * 后置通知
     * returnVal,切點(diǎn)方法執(zhí)行后的返回值
     */
    @AfterReturning(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))", returning = "returnVal")
    public void afterReturning(Object returnVal){
        System.out.println("后置通知...."+returnVal);
    }

    /**
     * 環(huán)繞通知
     * @param joinPoint 可用于執(zhí)行切點(diǎn)的類
     * @return
     * @throws Throwable
     */
    @Around("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("環(huán)繞通知前....");
        Object obj= joinPoint.proceed();
        System.out.println("環(huán)繞通知后....");
        return obj;
    }

    /**
     * 拋出通知
     * @param e
     */
    @AfterThrowing(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))", throwing = "e")
    public void afterThrowable(Throwable e){
        System.out.println("出現(xiàn)異常:msg="+e.getMessage());
    }

    /**
     * 無論什么情況下都會執(zhí)行的方法
     */
    @After(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))")
    public void after(){
        System.out.println("最終通知....");
    }
}

conf:

@Configuration
@EnableAspectJAutoProxy
public class ConfigOfAOP {

    @Bean
    public UserDao userDao(){
        return new UserDaoImpl();
    }

    @Bean
    public MyAspect myAspect(){
        return new MyAspect();
    }
}

測試類:

public class UserDaoAspectJTest {
    AnnotationConfigApplicationContext app = new AnnotationConfigApplicationContext(ConfigOfAOP.class);

    @Test
    public void aspectJTest(){
        UserDao userDao = app.getBean(UserDao.class);
        userDao.addUser();
    }
}

簡單說明一下符糊,定義了一個目標(biāo)類UserDaoImpl凫海,利用Spring2.0引入的aspect注解開發(fā)功能定義aspect類即MyAspect,在該aspect類中男娄,編寫了5種注解類型的通知函數(shù)行贪,分別是前置通知@Before、后置通知@AfterReturning模闲、環(huán)繞通知@Around瓮顽、異常通知@AfterThrowing、最終通知@After围橡,這5種通知與前面分析AspectJ的通知類型幾乎是一樣的暖混,并注解通知上使用execution關(guān)鍵字定義的切點(diǎn)表達(dá)式,即指明該通知要應(yīng)用的目標(biāo)函數(shù)翁授,當(dāng)只有一個execution參數(shù)時拣播,value屬性可以省略,當(dāng)含兩個以上的參數(shù)收擦,value必須注明贮配,如存在返回值時。當(dāng)然除了把切點(diǎn)表達(dá)式直接傳遞給通知注解類型外塞赂,還可以使用@pointcut來定義切點(diǎn)匹配表達(dá)式泪勒,這個與AspectJ使用關(guān)鍵字pointcut是一樣的,后面分析宴猾。
運(yùn)行程序圆存,結(jié)果符合預(yù)期:

20201015160104.png

3.2 AOP 術(shù)語

AOP 領(lǐng)域中的術(shù)語:

  • 通知(Advice): AOP 框架中的增強(qiáng)處理。通知描述了切面何時執(zhí)行以及如何執(zhí)行增強(qiáng)處理仇哆。
  • 連接點(diǎn)(join point): 連接點(diǎn)表示應(yīng)用執(zhí)行過程中能夠插入切面的一個點(diǎn)沦辙,這個點(diǎn)可以是方法的調(diào)用、異常的拋出讹剔。在 Spring AOP 中油讯,連接點(diǎn)總是方法的調(diào)用详民。
  • 切點(diǎn)(PointCut): 可以插入增強(qiáng)處理的連接點(diǎn)。
  • 切面(Aspect): 切面是通知和切點(diǎn)的結(jié)合陌兑。
  • 引入(Introduction):引入允許我們向現(xiàn)有的類添加新的方法或者屬性沈跨。
  • 織入(Weaving): 將增強(qiáng)處理添加到目標(biāo)對象中,并創(chuàng)建一個被增強(qiáng)的對象兔综,這個過程就是織入谒出。

3.3 定義切入點(diǎn)函數(shù)

在案例中,定義過濾切入點(diǎn)函數(shù)時邻奠,是直接把execution已定義匹配表達(dá)式作為值傳遞給通知類型的如下:

@After(value = "execution(* com.gaara.aop.dao.UserDao.addUser(..))")
public void after(){
    System.out.println("最終通知....");
}

除了上述方式外笤喳,還可采用與ApectJ中使用pointcut關(guān)鍵字類似的方式定義切入點(diǎn)表達(dá)式如下,使用@Pointcut注解:

/**
 * 使用Pointcut定義切點(diǎn)
 */
@Pointcut("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
private void myPointcut(){}

/**
 * 應(yīng)用切入點(diǎn)函數(shù)
 */
@After(value="myPointcut()")
public void after(){
    System.out.println("最終通知....");
}

3.4 切入點(diǎn)指示符

為了方法通知應(yīng)用到相應(yīng)過濾的目標(biāo)方法上碌宴,SpringAOP提供了匹配表達(dá)式杀狡,這些表達(dá)式也叫切入點(diǎn)指示符,在前面的案例中贰镣,它們已多次出現(xiàn)呜象。

3.4.1 通配符

在定義匹配表達(dá)式時,通配符幾乎隨處可見碑隆,如*恭陡、.. 、+ 上煤,它們的含義如下:

  • .. :匹配方法定義中的任意數(shù)量的參數(shù)休玩,此外還匹配類定義中的任意數(shù)量包
//任意返回值,任意名稱劫狠,任意參數(shù)的公共方法
execution(public * *(..))
//匹配com.gaara.aop.dao包及其子包中所有類中的所有方法
within(com.gaara.aop.dao..*)
  • + :匹配給定類的任意子類
//匹配實(shí)現(xiàn)了DaoUser接口的所有子類的方法
within(com.gaara.aop.dao.DaoUser+)
  • * :匹配任意數(shù)量的字符
//匹配com.gaara.service包及其子包中所有類的所有方法
within(com.gaara.service..*)
//匹配以set開頭拴疤,參數(shù)為int類型,任意返回值的方法
execution(* set*(int))

3.4.2 類型簽名表達(dá)式

為了方便類型(如接口独泞、類名呐矾、包名)過濾方法,Spring AOP 提供了within關(guān)鍵字懦砂。其語法格式如下:

within(<type name>)

type name 則使用包名或者類名替換即可蜒犯,來點(diǎn)案例吧。

//匹配com.gaara.aop.dao包及其子包中所有類中的所有方法
@Pointcut("within(com.gaara.aop.dao..*)")

//匹配UserDaoImpl類中所有方法
@Pointcut("within(com.gaara.aop.dao.UserDaoImpl)")

//匹配UserDaoImpl類及其子類中所有方法
@Pointcut("within(com.gaara.aop.dao.UserDaoImpl+)")

//匹配所有實(shí)現(xiàn)UserDao接口的類的所有方法
@Pointcut("within(com.gaara.aop.dao.UserDao+)")

3.4.3 方法簽名表達(dá)式

如果想根據(jù)方法簽名進(jìn)行過濾荞膘,關(guān)鍵字execution可以幫到我們罚随,語法表達(dá)式如下

//scope :方法作用域,如public,private,protect
//returnt-type:方法返回值類型
//fully-qualified-class-name:方法所在類的完全限定名稱
//parameters 方法參數(shù)
execution(<scope> <return-type> <fully-qualified-class-name>.*(parameters))

對于給定的作用域衫画、返回值類型毫炉、完全限定類名以及參數(shù)匹配的方法將會應(yīng)用切點(diǎn)函數(shù)指定的通知,這里給出模型案例:

//匹配UserDaoImpl類中的所有方法
@Pointcut("execution(* com.gaara.aop.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl類中的所有公共的方法
@Pointcut("execution(public * com.gaara.aop.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl類中的所有公共方法并且返回值為int類型
@Pointcut("execution(public int com.gaara.aop.dao.UserDaoImpl.*(..))")

//匹配UserDaoImpl類中第一個參數(shù)為int類型的所有公共的方法
@Pointcut("execution(public * com.gaara.aop.dao.UserDaoImpl.*(int , ..))")

3.4.4 其他指示符

  • bean:Spring AOP擴(kuò)展的削罩,AspectJ沒有對于指示符瞄勾,用于匹配特定名稱的Bean對象的執(zhí)行方法;
//匹配名稱中帶有后綴Service的Bean弥激。
@Pointcut("bean(*Service)")
private void myPointcut(){}
  • this :用于匹配當(dāng)前AOP代理對象類型的執(zhí)行方法进陡;請注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
//匹配了任意實(shí)現(xiàn)了UserDao接口的代理對象的方法進(jìn)行過濾
@Pointcut("this(com.gaara.dao.UserDao)")
private void myPointcut(){}
  • target :用于匹配當(dāng)前目標(biāo)對象類型的執(zhí)行方法微服;
//匹配了任意實(shí)現(xiàn)了UserDao接口的目標(biāo)對象的方法進(jìn)行過濾
@Pointcut("target(com.gaara.dao.UserDao)")
private void myPointcut(){}
  • @within:用于匹配所以持有指定注解類型內(nèi)的方法趾疚;請注意與within是有區(qū)別的, within是用于匹配指定類型內(nèi)的方法執(zhí)行以蕴;
//匹配使用了MarkerAnnotation注解的類(注意是類)
@Pointcut("@within(com.gaara.annotation.MarkerAnnotation)")
private void myPointcut(){}
  • @annotation: 根據(jù)所應(yīng)用的注解進(jìn)行方法過濾
//匹配使用了MarkerAnnotation注解的方法(注意是方法)
@Pointcut("@annotation(com.gaara.annotation.MarkerAnnotation)")
private void myPointcut(){}

關(guān)于表達(dá)式指示符就介紹到這糙麦,我們主要關(guān)心前面幾個常用的即可,不常用過印象即可丛肮。這里最后說明一點(diǎn)赡磅,切點(diǎn)指示符可以使用運(yùn)算符語法進(jìn)行表達(dá)式的混編,如and宝与、or焚廊、not(或者&&、||习劫、E匚痢),如下一個簡單例子:

//匹配了任意實(shí)現(xiàn)了UserDao接口的目標(biāo)對象的方法并且該接口不在com.gaara.dao包及其子包下
@Pointcut("target(com.gaara.spring.springAop.dao.UserDao) 诽里!within(com.gaara.dao..*)")
private void myPointcut(){}
//匹配了任意實(shí)現(xiàn)了UserDao接口的目標(biāo)對象的方法并且該方法名稱為addUser
@Pointcut("target(com.gaara.spring.springAop.dao.UserDao)&&execution(* com.gaara.spring.springAop.dao.UserDao.addUser(..))")
private void myPointcut(){}

3.5 通知函數(shù)以及傳遞參數(shù)

3.5.1 5種通知函數(shù)

通知在前面的aspectJ和Spring AOP的入門案例已見過面袒餐,在Spring中與AspectJ一樣,通知主要分5種類型谤狡,分別是前置通知匿乃、后置通知、異常通知豌汇、最終通知以及環(huán)繞通知幢炸,下面分別介紹。

  • 前置通知@Before
    前置通知通過@Before注解進(jìn)行標(biāo)注拒贱,并可直接傳入切點(diǎn)表達(dá)式的值宛徊,該通知在目標(biāo)函數(shù)執(zhí)行前執(zhí)行,注意JoinPoint逻澳,是Spring提供的靜態(tài)變量闸天,通過joinPoint 參數(shù),可以獲取目標(biāo)對象的信息,如類名稱,方法參數(shù),方法名稱等斜做,苞氮,該參數(shù)是可選的。
/**
 * 前置通知
 * @param joinPoint 該參數(shù)可以獲取目標(biāo)對象的信息,如類名稱,方法參數(shù),方法名稱等
 */
@Before("execution(* com.gaara.aop.dao.UserDao.addUser(..))")
public void before(JoinPoint joinPoint){
    System.out.println("我是前置通知");
}
  • 后置通知@AfterReturning
    通過@AfterReturning注解進(jìn)行標(biāo)注瓤逼,該函數(shù)在目標(biāo)函數(shù)執(zhí)行完成后執(zhí)行笼吟,并可以獲取到目標(biāo)函數(shù)最終的返回值returnVal库物,當(dāng)目標(biāo)函數(shù)沒有返回值時,returnVal將返回null贷帮,必須通過returning = “returnVal”注明參數(shù)的名稱而且必須與通知函數(shù)的參數(shù)名稱相同戚揭。請注意,在任何通知中這些參數(shù)都是可選的撵枢,需要使用時直接填寫即可民晒,不需要使用時,可以完成不用聲明出來锄禽。如下
/**
* 后置通知潜必,不需要參數(shù)時可以不提供
*/
@AfterReturning(value="execution(* com.gaara.aop.dao.UserDao.*User(..))")
public void AfterReturning(){
   System.out.println("我是后置通知...");
}
/**
* 后置通知
* returnVal,切點(diǎn)方法執(zhí)行后的返回值
*/
@AfterReturning(value="execution(* com.gaara.aop.dao.UserDao.*User(..))",returning = "returnVal")
public void AfterReturning(JoinPoint joinPoint,Object returnVal){
   System.out.println("我是后置通知...returnVal+"+returnVal);
}
  • 異常通知 @AfterThrowing
    該通知只有在異常時才會被觸發(fā),并由throwing來聲明一個接收異常信息的變量沃但,同樣異常通知也用于Joinpoint參數(shù)磁滚,需要時加上即可,如下:
/**
* 拋出通知
* @param e 拋出異常的信息
*/
@AfterThrowing(value="execution(* com.gaara.aop.dao.UserDao.addUser(..))",throwing = "e")
public void afterThrowable(Throwable e){
  System.out.println("出現(xiàn)異常:msg="+e.getMessage());
}
  • 最終通知 @After
    該通知有點(diǎn)類似于finally代碼塊绽慈,只要應(yīng)用了無論什么情況下都會執(zhí)行恨旱。
/**
 * 無論什么情況下都會執(zhí)行的方法
 * joinPoint 參數(shù)
 */
@After("execution(* com.gaara.aop.dao.UserDao.*User(..))")
public void after(JoinPoint joinPoint) {
    System.out.println("最終通知....");
}
  • 環(huán)繞通知@Around
    環(huán)繞通知既可以在目標(biāo)方法前執(zhí)行也可在目標(biāo)方法之后執(zhí)行,更重要的是環(huán)繞通知可以控制目標(biāo)方法是否指向執(zhí)行坝疼,但即使如此搜贤,我們應(yīng)該盡量以最簡單的方式滿足需求,在僅需在目標(biāo)方法前執(zhí)行時钝凶,應(yīng)該采用前置通知而非環(huán)繞通知仪芒。案例代碼如下第一個參數(shù)必須是ProceedingJoinPoint,通過該對象的proceed()方法來執(zhí)行目標(biāo)函數(shù)耕陷,proceed()的返回值就是環(huán)繞通知的返回值掂名。同樣的,ProceedingJoinPoint對象也是可以獲取目標(biāo)對象的信息,如類名稱,方法參數(shù),方法名稱等等哟沫。
@Around("execution(* com.gaara.aop.dao.UserDao.*User(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("我是環(huán)繞通知前....");
    //執(zhí)行目標(biāo)函數(shù)
    Object obj= joinPoint.proceed();
    System.out.println("我是環(huán)繞通知后....");
    return obj;
}

3.5.2 通知傳遞參數(shù)

在Spring AOP中饺蔑,除了execution和bean指示符不能傳遞參數(shù)給通知方法,其他指示符都可以將匹配的方法相應(yīng)參數(shù)或?qū)ο笞詣觽鬟f給通知方法嗜诀。獲取到匹配的方法參數(shù)后通過”argNames”屬性指定參數(shù)名猾警。如下,需要注意的是args(指示符)隆敢、argNames的參數(shù)名與before()方法中參數(shù)名 必須保持一致即param发皿。

@Before(value="args(param)", argNames="param") //明確指定了    
public void before(int param) {    
    System.out.println("param:" + param);    
}  

當(dāng)然也可以直接使用args指示符不帶argNames聲明參數(shù),如下:

@Before("execution(public * com.gaara..*.addUser(..)) && args(userId,..)")  
public void before(int userId) {  
    //調(diào)用addUser的方法時如果與addUser的參數(shù)匹配則會傳遞進(jìn)來會傳遞進(jìn)來
    System.out.println("userId:" + userId);  
} 

args(userId,..)該表達(dá)式會保證只匹配那些至少接收一個參數(shù)而且傳入的類型必須與userId一致的方法拂蝎,記住傳遞的參數(shù)可以簡單類型或者對象穴墅,而且只有參數(shù)和目標(biāo)方法也匹配時才有會有值傳遞進(jìn)來。

3.6 Aspect優(yōu)先級

在不同的切面中,如果有多個通知需要在同一個切點(diǎn)函數(shù)指定的過濾目標(biāo)方法上執(zhí)行玄货,那些在目標(biāo)方法前執(zhí)行(”進(jìn)入”)的通知函數(shù)皇钞,最高優(yōu)先級的通知將會先執(zhí)行,在執(zhí)行在目標(biāo)方法后執(zhí)行(“退出”)的通知函數(shù)誉结,最高優(yōu)先級會最后執(zhí)行鹅士。而對于在同一個切面定義的通知函數(shù)將會根據(jù)在類中的聲明順序執(zhí)行券躁。如下:

package com.gaara.aop.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AspectOne {
    @Pointcut("execution(* com.gaara.aop.dao.UserDao.deleteUser(..))")
    private void myPointcut(){}

    @Before("myPointcut()")
    public void beforeOne(){
        System.out.println("前置通知....執(zhí)行順序1");
    }

    @Before("myPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知....執(zhí)行順序2");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningThree(){
        System.out.println("后置通知....執(zhí)行順序3");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningFour(){
        System.out.println("后置通知....執(zhí)行順序4");
    }
}

在同一個切面中定義多個通知響應(yīng)同一個切點(diǎn)函數(shù)惩坑,執(zhí)行順序?yàn)槁暶黜樞颍?
Aspect優(yōu)先級

如果在不同的切面中定義多個通知響應(yīng)同一個切點(diǎn),進(jìn)入時則優(yōu)先級高的切面類中的通知函數(shù)優(yōu)先執(zhí)行,退出時則最后執(zhí)行,如下定義AspectOne類和AspectTwo類并實(shí)現(xiàn)org.springframework.core.Ordered接口,該接口用于控制切面類的優(yōu)先級,同時重寫getOrder方法,定制返回值,返回值(int類型)越小優(yōu)先級越大。其中AspectOne返回值為0,AspectTwo返回值為2,顯然AspectOne優(yōu)先級高于AspectTwo也拜。

package com.gaara.aop.aspect;

import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;


@Aspect
public class AspectOne implements Ordered{
    @Pointcut("execution(* com.gaara.aop.dao.UserDao.deleteUser(..))")
    private void myPointcut(){}

    @Before("myPointcut()")
    public void beforeOne(){
        System.out.println("前置通知..AspectOne..執(zhí)行順序1");
    }

    @Before("myPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知..AspectOne..執(zhí)行順序2");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningThree(){
        System.out.println("后置通知..AspectOne..執(zhí)行順序3");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningFour(){
        System.out.println("后置通知..AspectOne..執(zhí)行順序4");
    }

    /**
     * 定義優(yōu)先級,值越低,優(yōu)先級越高
     * @return
     */
    public int getOrder() {
        return 0;
    }
}


@Aspect
public class AspectTwo implements Ordered{
    @Pointcut("execution(* com.gaara.aop.dao.UserDao.deleteUser(..))")
    private void myPointcut(){}

    @Before("myPointcut()")
    public void beforeOne(){
        System.out.println("前置通知..AspectTwo..執(zhí)行順序1");
    }

    @Before("myPointcut()")
    public void beforeTwo(){
        System.out.println("前置通知..AspectTwo..執(zhí)行順序2");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningThree(){
        System.out.println("后置通知..AspectTwo..執(zhí)行順序3");
    }

    @AfterReturning(value = "myPointcut()")
    public void afterReturningFour(){
        System.out.println("后置通知..AspectTwo..執(zhí)行順序4");
    }

    /**
     * 定義優(yōu)先級,值越低,優(yōu)先級越高
     * @return
     */
    public int getOrder() {
        return 2;
    }
}

運(yùn)行結(jié)果如下:
aop2.png

雖然只演示了前置通知和后置通知以舒,但其他通知也遵循相同的規(guī)則。

四慢哈、 Spring AOP的實(shí)現(xiàn)原理概要

SpringAop的實(shí)現(xiàn)原理是基于動態(tài)織入技術(shù),而AspectJ則是靜態(tài)織入,而動態(tài)代理技術(shù)又分為Java JDK動態(tài)代理和CGLIB動態(tài)代理,前者是基于反射技術(shù)實(shí)現(xiàn),后者是基于繼承的機(jī)制實(shí)現(xiàn)蔓钟。

4.1 JDK動態(tài)代理

示例:

// 自定義的接口類,JDK動態(tài)代理的實(shí)現(xiàn)必須有對應(yīng)的接口類
public interface ExInterface {
    void execute();
}

// A類卵贱,實(shí)現(xiàn)了ExInterface接口類
public class A implements ExInterface{
    public void execute() {
        System.out.println("執(zhí)行A的execute方法...");
    }
}

// 代理類的實(shí)現(xiàn)
public class JDKProxy implements InvocationHandler{
    // 要被代理的目標(biāo)對象
    private A target;

    public JDKProxy(A target) {
        this.target = target;
    }

    /**
     * 創(chuàng)建代理類
     * @return
     */
    public ExInterface createProxy(){
        return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

    /**
     * 調(diào)用被代理類(目標(biāo)對象)的任意方法都會觸發(fā)invoke方法
     * @param proxy 代理類
     * @param method 被代理類的方法
     * @param args 被代理類的方法參數(shù)
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 過濾不需要該業(yè)務(wù)的方法
        if ("execute".equals(method.getName())){
            // 調(diào)用前驗(yàn)證權(quán)限
            System.out.println("執(zhí)行前驗(yàn)證用戶權(quán)限...");
            // 調(diào)用目標(biāo)對象的方法
            Object result = method.invoke(target, args);
            // 記錄日志數(shù)據(jù)
            System.out.println("記錄日志并上報(bào)...");
            return result;
        }else if ("delete".equals(method.getName())){

        }
        // 如果不需要增強(qiáng)直接執(zhí)行原方法
        return method.invoke(target, args);
    }
}

//測試
public void JDKProxyTest(){
        A a = new A();
        // 創(chuàng)建JDK代理
        JDKProxy jdkProxy = new JDKProxy(a);
        // 創(chuàng)建代理對象
        ExInterface proxy = jdkProxy.createProxy();
        // 執(zhí)行代理對象方法
        proxy.execute();
    }

運(yùn)行結(jié)果:
jdkproxy.png

在A的execute方法里面沒有任何權(quán)限和日志的代碼,也沒有直接操作a對象,相反的只是調(diào)用了proxy代理對象的方法,最終結(jié)果卻是預(yù)期的,這就是動態(tài)代理技術(shù),是不是跟SpringAOP似曾相識,實(shí)際上動態(tài)代理的底層是通過反射技術(shù)來實(shí)現(xiàn),只要拿到A類的class文件和A類的實(shí)現(xiàn)接口,很自然就可以生成相同接口的代理類并調(diào)用a對象的方法了,但實(shí)現(xiàn)java動態(tài)代理是有先決條件的,改條件是目標(biāo)對象必須帶接口,如A類的接口是ExInterface,通過ExInterface接口動態(tài)代理技術(shù)便可以創(chuàng)建于A類類型相同的代理對象滥沫。
代理對象的創(chuàng)建時通過Proxy類達(dá)到的,Proxy類由Java JDK提供,利用Proxy#newProxyInstance方法便可以動態(tài)生成代理對象,底層通過反射實(shí)現(xiàn),改方法需要3個參數(shù)

/**
* @param loader 類加載器,一般傳遞目標(biāo)對象(A類即被代理的對象)的類加載器
* @param interfaces 目標(biāo)對象(A)的實(shí)現(xiàn)接口
* @param h 回調(diào)處理句柄
*/
public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)

創(chuàng)建代理類proxy的代碼如下:

public ExInterface createProxy(){
        return (ExInterface) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }

到此并沒有結(jié)束,因?yàn)橛薪涌谶€是遠(yuǎn)遠(yuǎn)不夠,代理類還需要實(shí)現(xiàn)InvocationHandler接口,也是由JDK提供,代理類必須實(shí)現(xiàn)并重寫invoke方法,完全可以把InvocationHandler看成一個回調(diào)函數(shù),Proxy方法創(chuàng)建代理對象后,調(diào)用用execute方法時,將會回調(diào)InvocationHandler#invoke方法,因此我們可以在invoke方法中來控制被代理對象的方法執(zhí)行,從而在該方法前后動態(tài)增加其他需要執(zhí)行的業(yè)務(wù)键俱。

invoke方法有三個參數(shù):

  • Object proxy:生成的代理對象
  • Method method:目標(biāo)對象的方法,通過反射調(diào)用
  • Object[] args:目標(biāo)對象方法的參數(shù)

這就是是Java JDK動態(tài)代理的代碼實(shí)現(xiàn)過程,運(yùn)用JDK動態(tài)代理,被代理類,必須已有實(shí)現(xiàn)接口,因?yàn)镴DK提供的Proxy類將通過目標(biāo)對象的類加載器ClassLoader和Interface,以及句柄創(chuàng)建與A類擁有相同接口的代理對象proxy,改代理對象將擁有接口中的所有方法,同時代理類必須實(shí)現(xiàn)一個類似回調(diào)函數(shù)的InvocationHandler接口并重寫改接口中的invoke方法,當(dāng)調(diào)用proxy的每個方法時,invoke方法將被調(diào)用,利用該特性,可以在invoke方法中對目標(biāo)對象方法執(zhí)行的前后動態(tài)添加其他外圍業(yè)務(wù)操作,此時無需觸及目標(biāo)對象的任何代碼,也就實(shí)現(xiàn)了外圍業(yè)務(wù)的操作與目標(biāo)對象完全解耦合的目的兰绣。當(dāng)然缺點(diǎn)也很明顯,需要擁有接口,這也就有了后來的CGLIB動態(tài)代理了。

4.2 CGLIB動態(tài)代理

通過CGLIB動態(tài)代理實(shí)現(xiàn)上述功能并不要求目標(biāo)對象擁有接口類编振,實(shí)際上CGLIB動態(tài)代理是通過繼承的方式實(shí)現(xiàn)的缀辩,因此可以減少沒必要的接口,下面直接通過簡單案例協(xié)助理解

// 被代理的類
public class A {
    public void execute() {
        System.out.println("執(zhí)行A的execute方法...");
    }
}

// 代理類
public class CGLIBProxy implements MethodInterceptor{
    /**
     * 被代理的目標(biāo)類
     */
    private A target;

    public CGLIBProxy(A target) {
        super();
        this.target = target;
    }

    /**
     * 創(chuàng)建代理對象
     * @return
     */
    public A createProxy(){
        // 使用CGLIB生成代理
        // 1.聲明增強(qiáng)類實(shí)例,用于生產(chǎn)代理類
        Enhancer enhancer = new Enhancer();
        // 2.設(shè)置被代理類字節(jié)碼踪央,CGLIB根據(jù)字節(jié)碼生成被代理類的子類
        enhancer.setSuperclass(target.getClass());
        // 3.設(shè)置回調(diào)函數(shù)臀玄,即一個方法攔截
        enhancer.setCallback(this);
        // 創(chuàng)建代理
        return (A) enhancer.create();
    }

    /**
     * 回調(diào)函數(shù)
     * @param proxy 代理對象
     * @param method 委托類方法
     * @param args 方法參數(shù)
     * @param methodProxy 每個被代理的方法都對應(yīng)一個MethodProxy對象,
     *                    methodProxy.invokeSuper方法最終調(diào)用委托類(目標(biāo)類)的原始方法
     * @return
     * @throws Throwable
     */
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 過濾不需要該業(yè)務(wù)的方法
        if ("execute".equals(method.getName())){
            // 調(diào)用前驗(yàn)證權(quán)限
            System.out.println("執(zhí)行前驗(yàn)證用戶權(quán)限...");
            // 調(diào)用目標(biāo)對象的方法
            Object result = methodProxy.invokeSuper(proxy, args);
            // 記錄日志數(shù)據(jù)
            System.out.println("記錄日志并上報(bào)...");
            return result;
        }else if ("delete".equals(method.getName())){

        }
        // 如果不需要增強(qiáng)直接執(zhí)行原方法
        return methodProxy.invokeSuper(proxy, args);
    }
}

從代碼看被代理的類無需接口即可實(shí)現(xiàn)動態(tài)代理,而CGLIBProxy代理類需要實(shí)現(xiàn)一個方法攔截器接口MethodInterceptor并重寫intercept方法,類似JDk動態(tài)代理的InvocationHandler接口,也是理解為回調(diào)函數(shù),同理每次調(diào)用代理對象的方法時,intercept方法都會被調(diào)用,利用該方法便可以在運(yùn)行時對方法執(zhí)行前后進(jìn)行動態(tài)增強(qiáng)畅蹂。關(guān)于代理對象創(chuàng)建則通過Enhancer類來設(shè)置的,Enhancer是一個用于產(chǎn)生代理對象的類,作用類似JDK的Proxy類,因?yàn)镃GLIB底層是通過繼承實(shí)現(xiàn)的動態(tài)代理,因此需要傳遞目標(biāo)對象的Class,同時需要設(shè)置一個回調(diào)函數(shù)對調(diào)用方法進(jìn)行攔截并進(jìn)行相應(yīng)處理,最后create()創(chuàng)建目標(biāo)對象的代理對象,運(yùn)行結(jié)果與前面的JDK動態(tài)代理效果相同健无。
通過這些我們也應(yīng)該明白SpringAOP確實(shí)是通過CGLIB或者JDK代理來動態(tài)的生成代理對象,這個代理對象指的就是AOP代理類,而AOP代理類的方法則通過在目標(biāo)對象的切點(diǎn)動態(tài)地織入增強(qiáng)處理,從而完成了對目標(biāo)方法的增強(qiáng)。這里并沒有非常深入去分析這兩種技術(shù),只是演示了SpringAOP底層實(shí)現(xiàn)的最簡化的模型代碼,SpringAOP內(nèi)部已都實(shí)現(xiàn)了這兩種技術(shù),SpringAOP在使用時機(jī)上也進(jìn)行自動化調(diào)整,當(dāng)有接口時會自動選擇JDK動態(tài)代理技術(shù),如果沒有則選擇CGLIB技術(shù),當(dāng)然SpringAOP的底層實(shí)現(xiàn)并沒有這么簡單,為更簡便生成代理對象,SpringAOP內(nèi)部實(shí)現(xiàn)了一個專注于生成代理對象的工廠類,這樣就避免了大量的手動編碼,這一點(diǎn)也是十分人性化的,但最核心的還是動態(tài)代理技術(shù)液斜。

五累贤、 Spring AOP源碼解析

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市旗唁,隨后出現(xiàn)的幾起案子畦浓,更是在濱河造成了極大的恐慌,老刑警劉巖检疫,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讶请,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)夺溢,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進(jìn)店門论巍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人风响,你說我怎么就攤上這事嘉汰。” “怎么了状勤?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵鞋怀,是天一觀的道長。 經(jīng)常有香客問我持搜,道長密似,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任葫盼,我火速辦了婚禮残腌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘贫导。我一直安慰自己抛猫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布孩灯。 她就那樣靜靜地躺著闺金,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钱反。 梳的紋絲不亂的頭發(fā)上掖看,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機(jī)與錄音面哥,去河邊找鬼哎壳。 笑死,一個胖子當(dāng)著我的面吹牛尚卫,可吹牛的內(nèi)容都是我干的归榕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼吱涉,長吁一口氣:“原來是場噩夢啊……” “哼刹泄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起怎爵,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤特石,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后鳖链,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姆蘸,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了逞敷。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狂秦。...
    茶點(diǎn)故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖推捐,靈堂內(nèi)的尸體忽然破棺而出裂问,到底是詐尸還是另有隱情,我是刑警寧澤牛柒,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布堪簿,位于F島的核電站,受9級特大地震影響焰络,放射性物質(zhì)發(fā)生泄漏戴甩。R本人自食惡果不足惜符喝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一闪彼、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧协饲,春花似錦畏腕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至而线,卻和暖如春铭污,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背膀篮。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工嘹狞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人誓竿。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓磅网,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筷屡。 傳聞我的和親對象是個殘疾皇子涧偷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容