一就珠、 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的插件:如果使用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文件了品抽。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ù):我們發(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é)助理解。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)類投队。關(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ù)期:
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)槁暶黜樞颍? 如果在不同的切面中定義多個通知響應(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é)果如下: 雖然只演示了前置通知和后置通知以舒,但其他通知也遵循相同的規(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é)果: 在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ù)液斜。