zz 基於@AspectJ配置Spring AOP(之二)

    概述
    【IT168 專稿】在《基於@AspectJ配置 Spring AOP(之一)》的文章中,我們講解基於@AspectJ Spring AOP的基礎知識,在本文中,我們將繼續學習@AspectJ一些高級的知識。@AspectJ可以使用邏輯運算符對切點進行復合運算得到複合的切點;爲 了在切面中重用切點,我們還可以對切點進行命名,以便在其它的地方引用定義過的切點;當一個連接點匹配多個切點時,需要考慮織入順序的問題;此外,一個重 要的問題是如何在增強中訪問連接點上下文的信息。

    切點複合運算
    使用切點複合運算符,我們將擁有強大而靈活的切點表達能力,以下是一個使用了複合切點的切面:
    代碼清單 7 TestAspect:切點複合運算
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class TestAspect {
@After(
"within(com.baobaotao.*) "
+ " && execution(* greetTo(..)))") ①與運算
public void greeToFun() {
System.
out.println("--greeToFun() executed!--");
}


@Before(
" !target(com.baobaotao.NaiveWaiter) "+
"&& execution(* serveTo(..)))") ②非與運算
public void notServeInNaiveWaiter() {
System.
out.println("--notServeInNaiveWaiter() executed!--");
}

@AfterReturning(
"target(com.baobaotao.Waiter) || "+
" target(com.baobaotao.Seller)") ③或運算
public void waiterOrSeller(){
System.
out.println("--waiterOrSeller() executed!--");
}

}


    在①處,我們通過&&運算符定義了一個匹配com.baobaotao包中所有greetTo方法的切點;在②處,我們通過! 和&&運算符定義了一個匹配所有serveTo()方法並且該方法不位於NaiveWaiter目標類的切點;在③處,我們通過||運算 符定義了一個匹配Waiter和Seller接口實現類所有連接點的切點。


    命名切點
    在前面所舉的例子中,切點直接聲明在增強方法處,這種切點聲明方式稱爲匿名切點,匿名切點只能在聲明處使用。如果希望在其它地方重用一個切點,我們可以通過@Pointcut註解以及切面類方法對切點進行命名,以下是一個具體的實例:
代碼清單 8 TestNamePointcut
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Pointcut;
public class TestNamePointcut {
@Pointcut(
"within(com.baobaotao.*)") ①通過註解方法inPackage()對該切點進行命名,方法可視域
修飾符爲private,表明該命名切點只能在本切面類中使用。
private void inPackage(){}

    @Pointcut("execution(* greetTo(..)))") ②通過註解方法greetTo()對該切點進行命名,方法可視域
修飾符爲protected,表明該命名切點可以在當前包中的切面
類、子切面類中中使用。
    protected void greetTo(){}

    @Pointcut("inPackage() and greetTo()") ③引用命名切點定義的切點,本切點也是命名切點,
    它對應的可視域爲public
public void inPkgGreetTo(){}
}

    我們在代碼清單 8中定義了3個命名切點,命名切點的使用類方法作爲切點的名稱,此外方法的訪問修飾符還控制了切點的可引用性,這種可引用性和類方法的可訪問性相同,如 private的切點只能在本類中引用,public的切點可以在任何類中引用。命名切點僅利用方法名及訪問修飾符的信息,所以習慣上,方法的返回類型爲 void,並且方法體爲空。我們可以通過下圖更直觀地瞭解命名切點的結構:


圖 8 命名切點結構

    在③處,inPkgGreetTo()的切點引用了同類中的greetTo()切點,而inPkgGreetTo()切點可以被任何類引用。你還可以擴展TestNamePointcut類,通過類的繼承關係定義更多的切點。
    命名切點定義好後,就可以在定義切面時通過名稱引用切點,請看下面的實例:

package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class TestAspect {
@Before(
"TestNamePointcut.inPkgGreetTo()") ①
public void pkgGreetTo(){
System.
out.println("--pkgGreetTo() executed!--");
}

@Before(
"!target(com.baobaotao.NaiveWaiter) && TestNamePointcut.inPkgGreetTo()") ②
public void pkgGreetToNotNaiveWaiter(){
System.
out.println("--pkgGreetToNotNaiveWaiter() executed!--");
}

}

    在①處,我們引用了TestNamePointcut.inPkgGreetTo()切點,而在②處,我們在複合運算中使用了命名切點。







  增強織入的順序
    一個連接點可以同時匹配多個切點,切點對應的增強在連接點上的織入順序到底是如何安排呢?這個問題需要分三種情況討論:
 如果增強在同一個切面類中聲明,則依照增強在切面類中定義的順序進行織入;
 如果增強位於不同的切面類中,且這些切面類都實現了org.springframework.core.Ordered接口,則由接口方法的順序號決定(順序號小的先織入);
 如果增強位於不同的切面類中,且這些切面類沒有實現org.springframework.core.Ordered接口,織入的順序是不確定的。
我們可以通過下圖描述這種織入的規則:


圖 9 增強織入順序

    切面類A和B都實現爲Ordered接口,A切面類對應序號爲1,B切面類對應序號爲2,A切面類按順序定義了3個增強,B切面類按順序定義兩個增強,這5個增強對應的切點都匹配某個目標類的連接點,則增強織入的順序爲圖中虛線所示。
訪問連接點信息

    AspectJ使用org.aspectj.lang.JoinPoint接口表示目標類連接點對象,如果是環繞增強時,使用 org.aspectj.lang.ProceedingJoinPoint表示連接點對象,該類是JoinPoint的子接口。任何一個增強方法都可以 通過將第一個入參聲明爲JoinPoint訪問到連接點上下文的信息。我們先來了解一下這兩個接口的主要方法:
1)JoinPoint
 java.lang.Object[] getArgs():獲取連接點方法運行時的入參列表;
 Signature getSignature() :獲取連接點的方法簽名對象;
 java.lang.Object getTarget() :獲取連接點所在的目標對象;
 java.lang.Object getThis() :獲取代理對象本身;
2)ProceedingJoinPoint
ProceedingJoinPoint繼承JoinPoint子接口,它新增了兩個用於執行連接點方法的方法:
 java.lang.Object proceed() throws java.lang.Throwable:通過反射執行目標對象的連接點處的方法;
 java.lang.Object proceed(java.lang.Object[] args) throws java.lang.Throwable:通過反射執行目標對象連接點處的方法,不過使用新的入參替換原來的入參。
    讓我們來看一個具體的實例:
    代碼清單 9 TestAspect:訪問連接點對象

package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TestAspect {
@Around(
"execution(* greetTo(..)) && target(com.baobaotao.NaiveWaiter)") ①環繞增強
public void joinPointAccess(ProceedingJoinPoint pjp) throws Throwable{ ②聲明連接點入參
System.
out.println("------joinPointAccess-------");
③ 以下兩行訪問連接點信息
System.
out.println("args[0]:"+pjp.getArgs()[0]);
System.
out.println("signature:"+pjp.getTarget().getClass());
pjp.proceed(); ④ 通過連接點執行目標對象方法
System.
out.println("-------joinPointAccess-------");
}

}


    在①處,我們聲明瞭一個環繞增強,在②處增強方法的第一個入參聲明爲PreceedingJoinPoint類型(注意一定要在第一個位置),在③處,我們通過連接點對象pjp訪問連接點的信息。在④處,我們通過連接點調用目標對象的方法。
執行以下的測試代碼:

String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx
= new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter
= (Waiter) ctx.getBean("naiveWaiter");
naiveWaiter.greetTo(
"John");

    輸出以下的信息:
    ------joinPointAccess-------
    args[0]:John
    signature:class com.baobaotao.NaiveWaiter
    NaiveWaiter:greet to John... ①對應pjp.proceed();
    -------joinPointAccess-------


綁定連接點方法入參
我們介紹切點函數時說過args()、this()、target()、@args()、@within()、@target()和@annotation()這7個函數除了可以指定類名外,還可以指定參數名將目標對象連接點上的方法入參綁定到增強的方法中。
其中args()用於綁定連接點方法的入參,@annotation()用於綁定連接點方法的註解對象,而@args()用於綁定連接點方法入參的註解。來看一個args()綁定參數的實例:
代碼清單 10 TestAspect:綁定連接點參數
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class TestAspect {
①綁定連接點參數,首先args(name,num,..)根據②處的增強方法入參找到name和num對應的
類型,以得到真實的切點表達式:target(com.baobaotao.NaiveWaiter) && args(String,int,..)
在該增強方法織入到目標連接點時,增強方法可以通過num和name訪問到連接點方法的入參。
@Before("target(com.baobaotao.NaiveWaiter) && args(name,num,..)")
public void bindJoinPointParams(int num,String name){②增強方法接受連接點的參數
System.out.println("----bindJoinPointParams()----");
System.out.println("name:"+name);
System.out.println("num:"+num);
System.out.println("----bindJoinPointParams()----");
}
}
在①處,我們通過args(name,num,..)進行連接點參數的綁定,和前面我們所講述的方式不一樣,當args()函數入參爲參數名時,共包括兩方面的信息:
1) 連接點匹配規則信息:連接點方法第一個入參是String類型,第二個入參是int類型;
2) 連接點方法入參和增強方法入參的綁定信息:連接點方法的第一個入參綁定到增強方法的name參數上,第二個入參綁定到增強方法的num入參上。
切 點匹配和參數綁定的過程是這樣的:首先args()根據參數名稱在增強方法中查到名稱相同的入參並獲知對應的類型,這樣就知道匹配連接點方法的入參類型。 其次連接點方法入參類型所在的位置則由參數名在args()函數中聲明的位置決定。代碼清單 10中的args(name,num)只匹配第一個入參是String第二個入參是int的目標類方法,如smile(String name,int times)而不匹配smile(int times ,String anme)。我們可以通過以下示意圖詳細瞭解這一有趣的匹配過程:

圖 11 綁定參數和切點匹配過程
和args()一樣,其它可以綁定連接點參數的切點函數(如@args()和target()等),當指定參數名時,就同時具有匹配切點和綁定參數的雙重功能。
運行下面的測試:
String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
NaiveWaiter naiveWaiter = (NaiveWaiter) ctx.getBean("naiveWaiter");
naiveWaiter.smile("John",2);
我們將看到以下的輸出信息:
----bindJoinPointParams()----
name:John
num:2
----bindJoinPointParams()----
NaiveWaiter:smile to John2times...
可見,增強方法按預期綁定了NaiveWaiter.smile(String name,int times)方法的運行期入參。
提 示 爲了保證實例能成功執行,必須啓用CGLib動態代理:<aop:aspectj-autoproxy proxy-target-class="true" />,因爲該實例需要對NaiveWaiter類進行代理(因爲NaiveWaiter#simle()方法不是Waiter接口的方法),所以必 須使用CGLib生成子類的代理方法。
我們知道方法的入參名無法通過反射機制獲取,所以Spring按以下方式


綁定代理對象
使用this()或target()可綁定被代理對象實例,在通過類實例名綁定對象時,還依然具有原來連接點匹配的功能,只不過類名是通過增強方法中同名入參的類型間接決定罷了。這裏我們通過this()來了解對象綁定的用法:
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import com.baobaotao.Waiter;
@Aspect
public class TestAspect {
@Before("this(waiter)") ①通過②處查找出waiter對應的類型爲Waiter,因而切點表達式
爲this(Waiter),當增強方法織入目標連接點時,增強方法通過waiter
入參可以引用到代理對象的實例。
public void bindProxyObj(Waiter waiter){ ②
System.out.println("----bindProxyObj()----");
System.out.println(waiter.getClass().getName());
System.out.println("----bindProxyObj()----");
}
}
① 處的切點表達式首先按類變量名查找②處增強方法的入參列表,進而獲取類變量名對應的類爲com.baobaotao.Waiter,這樣就知道了切點的定 義爲this(com.baobaotao.Waiter),即所有代理對象爲Waiter類的所有方法匹配該切點。②處的增強方法通過waiter入參 綁定目標對象。
可見NaiveWaiter的所有方法匹配①處的切點,運行以下的測試代碼:
String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter = (Waiter) ctx.getBean("naiveWaiter");
naiveWaiter.greetTo("John");
可以看到如下的輸出信息:
----bindProxyObj()----
com.baobaotao.NaiveWaiter$$EnhancerByCGLIB$$6758891b
----bindProxyObj()----
NaiveWaiter:greet to John...
以按相似的方法使用target()進行綁定。
綁定類註解對象
@within()和@target()函數可以將目標類的註解對象綁定到增強方法中,我們通過@within()演示註解綁定的操作:
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import com.baobaotao.Monitorable;
@Aspect
public class TestAspect {
@Before("@within(m)") ①通過②處查找出m對應Monitorable類型的註解,
因而真實的切點表達式爲@within (Monitorable),當增強方法織入目標
連接點時,增強方法通過m入參可以引用到連接點處的註解對象。
public void bindTypeAnnoObject(Monitorable m){ ②
System.out.println("----bindTypeAnnoObject()----");
System.out.println(m.getClass().getName());
System.out.println("----bindTypeAnnoObject()----");
}
}
NaiveWaiter類中標註了@Monitorable註解,所有NaiveWaiter Bean匹配切點,其Monitorable註解對象將綁定到增強方法中。運行以下代碼,我們即可以查看到綁定註解對象:
String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter = (Waiter) ctx.getBean("naiveWaiter");
((NaiveWaiter)naiveWaiter).greetTo("John");
運行以上代碼,輸出以下信息:
----bindTypeAnnoObject()----
$Proxy3
----bindTypeAnnoObject()----
NaiveWaiter:greet to John...
從輸出信息中,我們還發現了一個祕密,即使用CGLib代理NaiveWaiter時,其類的註解Monitorable對象也被代理了。

   綁定返回值
    在後置增強中,可以通過returning綁定連接點方法的返回值:
@AfterReturning(value="target(com.baobaotao.SmartSeller)",returning="retVal") ①
public void bingReturnValue(int retVal){ ②
System.
out.println("----bindException()----");
System.
out.println("returnValue:"+retVal);
System.
out.println("----bindException()----");
}

①處和②處的名字必須相同,此外②處retVal的類型必須和連接點方法的返回值類型匹配。運行下面的測試代碼:
String configPath
= "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx
= new ClassPathXmlApplicationContext(configPath);
SmartSeller seller
= (SmartSeller) ctx.getBean("seller");
seller.sell(
"Beer","John");

    可以看到以下的輸出信息:
    SmartSeller: sell Beer to John...
    ----bingReturnValue()----
    returnValue:100
    ----bingReturnValue()----
    可見目標連接點Seller#sell()方法所返回的入參被成功綁定到增強方法中。

    綁定拋出的異常
    和通過切點函數綁定連接點信息不同,連接點拋出的異常必須使用AfterThrowing註解的throwing成員進行綁定:
    代碼清單 11 TestAspect:綁定異常對象
package com.baobaotao.aspectj.advanced;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class TestAspect {
@AfterThrowing(value
="target(com.baobaotao.SmartSeller)",throwing="iae") ①
public void bindException(IllegalArgumentException iae){ ②
System.
out.println("----bindException()----");
System.
out.println("exception:"+iae.getMessage());
System.
out.println("----bindException()----");
}

}


    ①處throwing指定的異常名和②處入參的異常名相同,這個異常增強只在連接點拋出的異常instanceof   IllegalArgumentException才匹配,增強方法通過iae參數可以訪問拋出的異常對象。
    我們在SmartSeller中添加一個拋出異常的測試方法:
package com.baobaotao;
public class SmartSeller implements Seller {
public void checkBill(int billId){
if(billId = = 1) throw new IllegalArgumentException("iae Exception");
else throw new RuntimeException("re Exception");
}

}


    當billId爲1時拋出IllegalArgumentException,否則拋出RuntimeException。運行以下測試代碼:
String configPath = "com/baobaotao/aspectj/advanced/beans.xml";
ApplicationContext ctx
= new ClassPathXmlApplicationContext(configPath);
SmartSeller seller
= (SmartSeller) ctx.getBean("seller");
seller.checkBill(
1); ① 運行該方法將引發IllegalArgumentException

    我們將看到以下的輸出信息:
    ----bindException()----
   exception:iae Exception
    ----bindException()----
    Exception in thread "main" java.lang.IllegalArgumentException: iae Exception
    …
    可見當seller.checkBill(1)拋出異常後,異常增強起效,處理完成後,再向外拋出IllegalArgumentException。如 果將①處的代碼調整爲seller.checkBill(2)後,再運行代碼,將只看到異常輸出的信息,異常增強沒有任何動作,這是因爲 RuntimeException 不按類型匹配於 IllegalArgumentException,切點不匹配。
    小結
    通過切點複合運算,你可以定義出各種複雜的切點,使切點表達式的能力進一步提升。你可以直接使用切點複合運算符對切點函數進行運算,也可以通過切點名引用 其它命名切點。當對同一個連接點織入多個增強時,你必須考慮讓切面類實現Ordered接口,此外還必須合理計劃同一個切面類中增強方法的聲明順序,因爲 這些信息都會影響到增強的織入順序。
在@AspectJ的切點表達式中,大多數的切點函數都可以綁定連接點方法的入參,以便增強方法訪問連接點信息。此外,你也可以簡單地將增強方法的第一個入參定義爲JoinPoint訪問連接點的上下文。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章