zz 基於@AspectJ配置Spring AOP

概述

    在低版本Spring中定義一個切面是比較麻煩的,需要實現特定的接口,並進行一些較爲複雜的配置,低版本Spring AOP的配置是被批評最多的地方。Spring聽取這方面的批評聲音,並下決心徹底改變這一現狀。在Spring2.0中,Spring AOP已經煥然一新,你可以使用@AspectJ註解非常容易的定義一個切面,不需要實現任何的接口。

Spring2.0採用 @AspectJ註解對POJO進行標註,從而定義一個包含切點信息和增強橫切邏輯的切面,Spring 2.0可以將這個切面織入到匹配的目標Bean中。@AspectJ註解使用AspectJ切點表達式語法進行切點定義,可以通過切點函數、運算符、通配 符等高級功能進行切點定義,擁有強大的連接點描述能力。在你學習基於@AspectJ的切面技術後,恐怕你就再也沒有興趣使用低版本Spring AOP的實現技術了,畢竟馬落桃花馬前雪,兩者的易用性、便捷性是不可同日而語的。

着手使用@AspectJ

    我們知道在低版本的Spring AOP中,你必須使用Pointcut和Advice接口描述切點和增強,並用Advisor組合兩者描述一個切面,@AspectJ則採用JDK 5.0的註解技術描述切點和增強類型,而增強的橫切邏輯就在被標註的POJO中定義。

使用前的準備

    在使用@AspectJ之前,首先你得保證你所使用的JDK的版本是5.0及以上版本,否則無法使用註解技術。 Spring在處理@Aspect註解表達式時,需要使用位於<SPRING_HOME>/lib/asm/目錄下asm的類包:asm- 2.2.2.jar、asm-commons-2.2.2.jar和asm-util-2.2.2.jar。asm是輕量級的字節碼處理框架,因爲 Java的反射機制無法獲取入參名,Spring就利用asm處理@AspectJ中所描述的方法入參名。

此外,Spring採用AspectJ提供的@AspectJ註解類庫及相應的解析類庫,它位於<SPRING_HOME>/lib/aspectj目錄下,將目錄下的aspectjrt.jar和aspectjweaver.jar類包加入類路徑中。

一個簡單的例子

    在做好上節中所提到的前置工作後,我們就可以開始編寫一個基於@AspectJ的切面了,首先來看一個簡單的例子,以便對@AspectJ有一個切身的認識。

下面,我們用@AspectJ註解對一個POJO進行標註,將使其成爲一個切面類:
代碼清單 1 PreGreetingAspect:切面
package com.baobaotao.aspectj.aspectj;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect ①通過該註解將PreGreetingAspect標識爲一個切面
public class PreGreetingAspect{
@Before("execution(* greetTo(..))") ②定義切點和增強類型
public void beforeGreeting(){③增強的橫切邏輯
System.out.println("How are you");
}
}

     我們“驚奇”地發現這個切面沒有實現任何特殊的接口,它只是一個普通的POJO。它特殊的地方在於使用了@AspectJ註解。
首先,在PreGreetingAspect類定義處,標註了一個@Aspectj註解,第三方處理程序就可以通過類是否擁有@Aspectj註解判斷其是否是一個切面,如①所示。

其 次,在beforeGreeting()方法標籤處,標註了@Before註解,併爲該註解提供了成員值"execution(* greetTo(..))",如②所示。②處的註解提供了兩個信息:@Before註解表示該增強是前置增強,而成員值通過@ApsectJ切點表達式語 法定義切點:即在目標類的greetTo()方法上織入增強,greetTo()方法可以帶任意的入參和任意的返回值。

最後,在③處的beforeGreeting()方法是增強的橫切邏輯,該橫切邏輯在目標方法前調用,我們通過下圖描述這種關係:
   

 圖 1 切面的信息構成

    PreGreetingAspect類通過註解和代碼,將切點、增強類型和增強的橫切邏輯揉合到一個類中,使切面的定義渾然天成。如果在低版本Spring AOP中,你必須同時創建增強類,切點類以及切面類,並使三者聯合表達相同的信息。

NaiveWaiter是一個Bean,它擁有一個greetTo()的方法,這個方法連接點匹配於上面我們通過@AspectJ所定義的切點,爲了方便後續的說明,我們給出NaiveWaiter的代碼:

package com.baobaotao;
public class NaiveWaiter implements Waiter {
public
void greetTo(String clientName) {
System.out.println(
"NaiveWaiter:greet to "+clientName+"...");
}
public
void serveTo(String clientName){
System.out.println(
"NaiveWaiter:serving "+clientName+"...");
}
}


下面,我們通過org.springframework.aop.aspectj.annotation.AspectJProxyFactory爲NaiveWaiter生成織入PreGreetingAspect切面的代理,如代碼清單 2所示:
代碼清單 2 AspectJProxyTest
package com.baobaotao.aspectj.example;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
import com.baobaotao.NaiveWaiter;
import com.baobaotao.Waiter;
public class AspectJProxyTest {
Waiter target
= new NaiveWaiter();
AspectJProxyFactory factory
= new AspectJProxyFactory();
factory.setTarget(target); ① 設置目標對象
factory.addAspect(PreGreetingAspect.
class); ②添加切面類
Waiter proxy
= factory.getProxy(); ③ 生成織入切面的代理對象
proxy.greetTo(
"John");
proxy.serveTo(
"John");
}
}

Spring使用AspectJProxyFactory織入基於@AspectJ切面的工作。在①處,設置了目標對象,在②處添加一個切面類,該類必須是帶@AspectJ註解的類,在③處,我們就可以獲取織入切面的代理對象了。
接下來,我們直接通過代理對象調用greetTo()和serveTo()代碼,它們產生以下的輸出信息:
How are you ①表示greetTo()方法被成功地織入了切面
greet to John...
serving John...
通過①處的輸出信息我們可以知道代理對象的greetTo()方法已經織入了切面類所定義的增強邏輯了。

通過配置織入@AspectJ切面

雖然可以通過編程的方式織入切面,但一般情況下,我們還是使用Spring的配置自動完成創建代理織入切面的工作。
<bean id="waiter" class="com.baobaotao.NaiveWaiter" /> ①目標Bean
②使用了@AspectJ註解的切面類
<bean class="com.baobaotao.aspectj.example.PreGreetingAspect" />
③自動代理創建器,自動將@AspectJ註解切面類織入到目標Bean中
<bean class="org.springframework.aop.aspectj.annotation.
AnnotationAwareAspectJAutoProxyCreator"/>

AnnotationAwareAspectJAutoProxyCreator能夠將@AspectJ註解切面的自動織入到目標Bean中。這 裏,PreGreetingAspect是使用了@AspectJ註解描述的切面類,而NaiveWaiter是匹配切點的目標類。

如果使用基於Schema的aop命名空間進行配置,事情就更簡單了:
<?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:aop
="http://www.springframework.org/schema/aop"
xsi:schemaLocation
="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop ②
http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
<aop:aspectj-autoproxy /> ③基於@AspectJ切面的驅動器
<bean id="waiter" class="com.baobaotao.NaiveWaiter" />
<bean class="com.baobaotao.aspectj.example.PreGreetingAspect" />
</beans>

首先,在配置文件中引入aop命名空間,如①、②處所示,然後通過aop命名空間的<aop:aspectj-autoproxy />聲明自動爲Spring容器中那些匹配@AspectJ切面的Bean創建代理,織入切面。當然,Spring在內部依舊採用 AnnotationAwareAspectJAutoProxyCreator進行自動代理的創建工作,但具體實現的細節已經 被<aop:aspectj-autoproxy />隱藏起來了。

<aop:aspectj-autoproxy />有一個proxy-target-class屬性,默認爲false,表示使用JDK動態代理織入增強,當配置 爲<aop:aspectj-autoproxy proxy-target-class="true" />時,表示使用CGLib動態代理技術織入增強。不過即使proxy-target-class設置爲false,如果目標類沒有聲明接口,則 Spring將自動使用CGLib動態代理。

@AspectJ語法基礎

@AspectJ使用JDK 5.0註解和正規的AspectJ 5的切點表達式語言描述切面,由於Spring只支持方法的連接點,所以Spring僅支持部分AspectJ的切點語言。在這節時,我們將對AspectJ切點表達式語言進行必要的學習。

切點表達式函數

    AspectJ 5的切點表達式由關鍵字和操作參數組成,如execution(* greetTo(..))的切點表達式,“execute”爲關鍵字,而“* greetTo(..)”爲操作參數。在這裏,execute代表目標類執行某一方法,而“* greetTo(..)”是描述目標方法的匹配模式串,兩者聯合起來所表示的切點匹配目標類greetTo()方法的連接點。爲了描述方便,我們將 execution()稱作函數,而將匹配串“* greetTo(..)”稱作函數的入參。
Spring支持9個@ApsectJ切點表達式函數,它們用不同的方式描述目標類的連接點,根據描述對象的不同,可以將它們大致分爲4種類型:
 方法切點函數:通過描述目標類方法信息定義連接點;
 方法入參切點函數:通過描述目標類方法入參的信息定義連接點;
 目標類切點函數:通過描述目標類類型信息定義連接點;
 代理類切點函數:通過描述目標類的代理類的信息定義連接點;
     這4種類型的切點函數,通過表 1進行說明:
    表 1 切點函數
 
類別
函數
入參
說明
方法切點函數
execution()
方法
匹配模式串
表示滿足某一匹配模式的所有目標類方法連接點。如execution(* greetTo(..))表示所有目標類中的greetTo()方法。
@annotation()
方法注
解類名
表示標註了特定註解的目標方法連接點。如@annotation(com.baobaotao.anno.NeedTest)表示任何標註了@NeedTest註解的目標類方法。
方法入參切點函數
args()
類名
通過判別目標類方法運行時入參對象的類型定義指定連接點。如args(com.baobaotao.Waiter)表示所有有且僅有一個按類型匹配於Waiter的入參的方法。
@args()
類型注
解類名
通過判別目標方法的運行時入參對象的類是否標註特定註解來指定連接點。如@args(com.baobaotao.Monitorable)表示任何這樣的一個目標方法:它有一個入參且入參對象的類標註@Monitorable註解。
目標類切點函數
within()
類名匹配串
   表 示特定域下的所有連接點。如within(com.baobaotao.service.*)表示com.baobaotao.service包中的所有 連接點,也即包中所有類的所有方法,而within(com.baobaotao.service.*Service)表示在 com.baobaotao.service包中,所有以Service結尾的類的所有連接點。
target()
類名
   假如目標類按類型匹配於指定類,則目標類的所有連接點匹配這個切點。如通過target(com.baobaotao.Waiter)定義的切點,Waiter、以及Waiter實現類NaiveWaiter中所有連接點都匹配該切點。
@within()
類型註解類名
   假如目標類按類型匹配於某個類A,且類A標註了特定註解,則目標類的所有連接點匹配這個切點。
   如@within(com.baobaotao.Monitorable)定義的切點,假如Waiter類標註了@Monitorable註解,則Waiter以及Waiter實現類NaiveWaiter類的所有連接點都匹配。
@target()
類型註解類名
   目標類標註了特定註解,則目標類所有連接點匹配該切點。如@target(com.baobaotao.Monitorable),假如NaiveWaiter標註了@Monitorable,則NaiveWaiter所有連接點匹配切點。
代理類切點函數
this()
類名
 代理類按類型匹配於指定類,則被代理的目標類所有連接點匹配切點。這個函數比較難理解,這裏暫不舉例,留待後面詳解。
 

@AspectJ除上表中所列的函數外,還有call()、initialization()、 preinitialization()、 staticinitialization()、 get()、 set()、handler()、 adviceexecution()、 withincode()、 cflow()、 cflowbelow()、 if()、 @this()以及@withincode()等函數,這些函數在Spring中不能使用,否則會拋出IllegalArgumentException 異常。在不特別聲明的情況下,本書中所講@AspectJ函數均指表 1中所列的函數。

的控制流。After註解類擁有2個成員:
 value:該成員用於定義切點;
 argNames:如前所述。

@DeclareParents
引介增強,相當於IntroductionInterceptor,DeclareParents註解類擁有2個成員:
 value:該成員用於定義切點,它表示在哪個目標類上添加引介增強;
 defaultImpl:默認的接口實現類。

除引介增強外,其它增強都很容易理解,我們將在本文後續內容中統一講述,但引介增強的使用比較特別,因爲我們特別在下節中爲其準備了一個實例。

引介增強用法
請看以下兩個接口及其實現類,如圖 2所示:

圖 2 Waiter和Seller

假設我們希望NaiveWaiter能夠同時充當售貨員的角色,即通過切面技術爲NaiveWaiter新增Seller接口的實現。我們可以使用@AspectJ的引介增強來實現這一功能。
代碼清單 3 EnableSellerAspect
package com.baobaotao.aspectj.basic; 
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
import com.baobaotao.Seller;
import com.baobaotao.SmartSeller;

@Aspect
public class EnableSellerAspect {
①爲NaiveWaiter添加接口實現
@DeclareParents(value="com.baobaotao.NaiveWaiter",
defaultImpl=SmartSeller.class) ② 默認的接口實現類
public Seller seller; ③要實現的目標接口
}
    在EnableSellerAspect切面中,我們通過@DeclareParents爲NaiveWaiter添加了一個需要實現的Seller接 口,並指定其默認實現類爲SmartSeller,然後通過切面技術將SmartSeller融合到NaiveWaiter中,這樣 NaiveWaiter就實現Seller接口了。
在Spring配置文件中配置好切面和NaiveWaiter Bean:
<aop:aspectj-autoproxy/>
<bean id="waiter" class="com.baobaotao.NaiveWaiter"/>
<bean class="com.baobaotao.aspectj.basic.EnableSellerAspect"/>

運行以下測試代碼:
package com.baobaotao.aspectj.basic;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.baobaotao.Seller;
import com.baobaotao.Waiter;
public class DeclaredParentsTest ...{
public static void main(String[] args) ...{
String configPath = "com/baobaotao/aspectj/basic/beans.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);
Waiter waiter = (Waiter)ctx.getBean("waiter");
waiter.greetTo("John");
Seller seller = (Seller)waiter; ①可以成功進行強制類型轉換
seller.sell("Beer", "John");
}
}


代碼成功執行,並輸出以下信息:
NaiveWaiter:greet to John...
SmartSeller: sell Beer to John...
可見,NaiveWaiter已經成功地新增了Seller接口的實現。

切點函數詳解

切點函數是AspectJ表達式語言的核心,是使用@AspectJ進行切面定義的難點,本節我們通過具體實例對切點函數進行深入學習。爲了方便講解,我們假設目標類包括以下7個類,這些目標類都位於com.baobaotao.*包中:

圖 3 Waiter和Seller類圖
這些類中,除了SmartSeller#showGoods()方法是protected外,其它的方法都是public。

@annotation()

@annotation表示標註了某個註解的所有方法。我們通過一個實例說明@annotation()的用法,TestAspect定義了一個後置增強切面,該增強將應用到標註有NeedTest的目標方法中:
package com.baobaotao.aspectj.fun;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class TestAspect {
@AfterReturning(
"@annotation(com.baobaotao.anno.NeedTest)") ①後置增強切面
public void needTestFun(){
System.
out.println("needTestFun() executed!");
}
}

假設NaughtyWaiter#greetTo()方法標註了@NeedTest註解,而NaiveWaiter#greetTo()方法沒有標註@NeedTest註解,如代碼清單 4所示:
代碼清單 4 標註了NeedTest註解的NaughtyWaiter

package com.baobaotao;
import com.baobaotao.anno.NeedTest;
public class NaughtyWaiter implements Waiter {
@NeedTest
public void greetTo(String clientName) {
System.
out.println("NaughtyWaiter:greet to "+clientName+"...");
}

}

通過Spring配置自動應用切面:

<aop:aspectj-autoproxy />
<bean id="naiveWaiter" class="com.baobaotao.NaiveWaiter" />
<bean id="naughtyWaiter" class="com.baobaotao.NaughtyWaiter" />
<bean class="com.baobaotao.aspectj.fun.TestAspect" />
運行以下的代碼:
代碼清單 5 PointcutFunTest:測試代碼

package com.baobaotao.aspectj.fun;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.baobaotao.Seller;
import com.baobaotao.Waiter;
public class PointcutFunTest{
public static void main(String[] args) {
String configPath
= "com/baobaotao/aspectj/fun/beans.xml";
ApplicationContext ctx
= new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter
= (Waiter) ctx.getBean("naiveWaiter");
Waiter naughtyWaiter
= (Waiter) ctx.getBean("naughtyWaiter");
naiveWaiter.greetTo(
"John"); ①該方法未被織入增強
naughtyWaiter.greetTo(
"Tom");②該方法被織入增強
}
}

輸出以下信息:
NaiveWaiter:greet to John...
NaughtyWaiter:greet to Tom... ①對應NaughtyWaiter的greetTo()
needTestFun() executed!
從以上的信息中,我們可以獲知切面被正確地織入到NaughtyWaiter#greetTo()方法中。

execution()
execution()是最常用的切點函數,其語法如下所示:

execution(<修飾符模式>? <返回類型模式> <方法名模式>(<參數模式>) <異常模式>?)

除了返回類型模式、方法名模式和參數模式外,其它項都是可選的。與其直接講解該方法的使用規則,還不如通過一個個具體的例子進行理解。下面,我們給出各種使用execution()函數實例。

1)通過方法簽名定義切點
 execution(public * *(..))
匹配所有目標類的public方法,但不匹配SmartSeller和protected void showGoods()方法。第一個*代表返回類型,第二個*代表方法名,而..代表任意入參的方法;

 execution(* *To(..))
匹配目標類所有以To爲後綴的方法。它匹配NaiveWaiter和NaughtyWaiter的greetTo()和serveTo()方法。第一個*代表返回類型,而*To代表任意以To爲後綴的方法;

2)通過類定義切點
 execution(* com.baobaotao.Waiter.*(..))
匹配Waiter接口的所有方法,它匹配NaiveWaiter和NaughtyWaiter類的greetTo()和serveTo()方法。第一個*代表返回任意類型,com.baobaotao.Waiter.*代表Waiter接口中的所有方法;

 execution(* com.baobaotao.Waiter+.*(..))
匹 配Waiter接口及其所有實現類的方法,它不但匹配NaiveWaiter和NaughtyWaiter類的greetTo()和serveTo()這 兩個Waiter接口定義的方法,同時還匹配NaiveWaiter#smile()和NaughtyWaiter#joke()這兩個不在Waiter 接口中定義的方法。

3)通過類包定義切點
在類名模式串中,“.*”表示包下的所有類,而“..*”表示包、子孫包下的所有類。
 execution(* com.baobaotao.*(..))
匹配com.baobaotao包下所有類的所有方法;

 execution(* com.baobaotao..*(..))
匹 配com.baobaotao包、子孫包下所有類的所有方法,如com.baobaotao.dao,com.baobaotao.servier以及 com.baobaotao.dao.user包下的所有類的所有方法都匹配。“..”出現在類名中時,後面必須跟“*”,表示包、子孫包下的所有類;

 execution(* com..*.*Dao.find*(..))
匹配包名前綴爲com的任何包下類名後綴爲Dao的方法,方法名必須以find爲前綴。如com.baobaotao.UserDao#findByUserId()、com.baobaotao.dao.ForumDao#findById()的方法都匹配切點。

4)通過方法入參定義切點
切點表達式中方法入參部分比較複雜,可以使用“*”和“ ..”通配符,其中“*”表示任意類型的參數,而“..”表示任意類型參數且參數個數不限。

 execution(* joke(String,int)))
匹 配joke(String,int)方法,且joke()方法的第一個入參是String,第二個入參是int。它匹配 NaughtyWaiter#joke(String,int)方法。如果方法中的入參類型是java.lang包下的類,可以直接使用類名,否則必須使 用全限定類名,如joke(java.util.List,int);

 execution(* joke(String,*)))
匹 配目標類中的joke()方法,該方法第一個入參爲String,第二個入參可以是任意類型,如joke(String s1,String s2)和joke(String s1,double d2)都匹配,但joke(String s1,double d2,String s3)則不匹配;

 execution(* joke(String,..)))
匹配目標類中的joke()方法,該方法第 一個入參爲String,後面可以有任意個入參且入參類型不限,如joke(String s1)、joke(String s1,String s2)和joke(String s1,double d2,String s3)都匹配。

 execution(* joke(Object+)))
匹 配目標類中的joke()方法,方法擁有一個入參,且入參是Object類型或該類的子類。 它匹配joke(String s1)和joke(Client c)。如果我們定義的切點是execution(* joke(Object)),則只匹配joke(Object object)而不匹配joke(String cc)或joke(Client c)。

args()和@args()
args()函數的入參是類名,@args()函數的入參必須是註解類的類名。雖然args()允許在類名後使用+通配符後綴,但該通配符在此處沒有意義:添加和不添加效果都一樣。

1)args()
該函數接受一個類名,表示目標類方法入參對象按類型匹配於指定類時,切點匹配,如下面的例子:
args(com.baobaotao.Waiter)
表 示運行時入參是Waiter類型的方法,它和execution(* *(com.baobaotao.Waiter))區別在於後者是針對類方法的簽名而言的,而前者則針對運行時的入參類型而言。如 args(com.baobaotao.Waiter)既匹配於addWaiter(Waiter waiter),也匹配於addNaiveWaiter(NaiveWaiter naiveWaiter),而execution(* *(com.baobaotao.Waiter))只匹配addWaiter(Waiter waiter)方法;實際上,args(com.baobaotao.Waiter)等價於execution(* *(com.baobaotao.Waiter+)),當然也等價於args(com.baobaotao.Waiter+)。

2)@args()
該函數接受一個註解類的類名,當方法的運行時入參對象標註髮指定的註解時,方法匹配切點。這個切點函數的匹配規則不太容易理解,我們通過以下示意圖對此進行詳細講解:

            圖 4 @arg(M)匹配示意圖(1)
    T0、T1、T2、T3具有如圖所示的繼承關係,假設目標類方法的簽名爲fun(T1 t),它的入參爲T1,而切面的切點定義爲@args(M),T2類標註了@M。當fun(T1 t)傳入對象是T2或T3時,則方法匹配@args(M)所聲明定義的切點;

再看下面的情況,假設方法簽名是fun(T1 t),入參對於T1,而標註@M的類是T0,當funt(T1 t)傳入T1、T2、T3的實例時,均不匹配切點@args(M)。

            圖 5 @arg(M)匹配示意圖(2)
    在類的繼承樹中,①點爲方法簽名中入參類型在類繼承樹中的位置,我們稱之爲入參類型點,而②爲標註了@M註解的類在類繼承樹中位置,我們稱之爲註解點。判斷方法在運行時是否匹配@agrs(M)切點,可以根據①點和②點在類繼承樹中的相對位置來判別:
1) 如果在類繼承樹中註解點②高於入參類型點①,則該目標方法不可能匹配切點@args(M),如圖 5所示;
2) 如果在類繼承樹中註解點②低於入參類型點①,則註解點所在類及其子孫類作爲方法入參時,該方法匹配@args(M)切點,如圖 4所示。
下 面舉一個具體的例子,假設我們定義這樣的切點:@args(com.baobaotao.Monitorable) ,如果NaiveWaiter標註了@Monitorable,則對於WaiterManager#addWaiter(Waiter w)方法來說,如果入參是NaiveWaiter或其子類對象,該方法匹配切點,如果入參是NaughtyWaiter對象,不匹配切點。如果 Waiter標註了@Monitorable,但NaiveWaiter未標註@Monitorable,則 WaiterManager#addNaiveWaiter(NaiveWaiter w)卻不匹配切點,這是因爲註解點(在Waiter)高於入參類型點(NaiveWaiter)。

within()

    通過類匹配模式串聲明切點,within()函數定義的連接點是針對目標類而言,而非針對運行期對象的類型而言,這一點和execetion()是相同 的。但和execution()函數不同的是,within()所指定的連接點最小範圍只能是類,而execution()所指定的連接點,可以大到包, 小到方法入參。所以從某種意義上說,execution()函數的功能涵蓋了within()函數的功能。within()函數的語法如下所示:
within(<類匹配模式>)

形如within(com.baobaotao.NaiveWaiter)是within()函數所能表達的最小粒度,如果試圖用within()匹配方 法級別的連接點,如within(com.baobaotao.NaiveWaiter.greet*)將會產生解析錯誤。

下面是一些使用within()函數的實例:
 within(com.baobaotao.NaiveWaiter)
匹 配目標類NaiveWaiter的所有方法。如果切點調整爲within(com.baobaotao.Waiter),則NaiveWaiter和 NaughtyWaiter中的所有方法都不匹配,而Waiter本身是接口不可能實例化,所以 within(com.baobaotao.Waiter)的聲明是無意義的;

 within(com.baobaotao.*)
匹配com.baobaotao包中的所有類,但不包括子孫包,所以com.baobaotao.service包中類的方法不匹配這個切點;

 within(com.baobaotao..*)
匹配com.baobaotao包及子孫包中的類,所以com.baobaotao.service、com.baobaotao.dao以及com.baobaotao.service.fourm等包中所有類的方法都匹配這個切點。

@within()和@target()

除 @annotation()和@args()外,還有另外兩個用於註解的切點函數,它們分別是@target()和@within(),和 @annotation()及@args()函數一樣,它們也只接受註解類名作爲入參。其中@target(M)匹配任意標註了@M的目標類,而 @within(M)匹配標註了@M的類及子孫類。
@target(M)切點的匹配規則如圖 6所示:

圖 6 @target(M)匹配目標類示意圖
假 設NaiveWaiter標註了@Monitorable,則其子類CuteNaiveWaiter沒有標註@Monitorable,則 @target(com.baobaotao.Monitorable)匹配NaiveWaiter類的所有方法,但不匹配 CuteNaiveWaiter類的方法。
@within(M)切點的匹配規則如圖 7所示:

圖 7 @within(M)匹配目標類示意圖

假 設NaiveWaiter標註了@Monitorable,而其子類CuteNaiveWaiter沒有標註@Monitorable,則 @within(com.baobaotao.Monitorable)不但匹配NaiveWaiter類中的所有方法也匹配 CuteNaiveWaiter類中的所有方法。

但有一個特別值得注意地方是,如果標註@M註解的是一個接口,則所有實現該接口的類並不 匹配@within(M)。假設Waiter標註了@Monitorable註解,但NaiveWaiter、NaughtyWaiter及 CuteNaiveWaiter這些接口實現類都沒有標註@Monitorable,則 @within(com.baobaotao.Monitorable)和@target(com.baobaotao.Monitorable)都不匹 配NaiveWaiter、NaughtyWaiter及CuteNaiveWaiter。這是因爲@within()、@target()以及 @annotation()都是針對目標類而言,而非針對運行時的引用類型而言,這點區別需要在開發中特別注意。

target()的this()

    target()切點函數通過判斷目標類是否按類型匹配指定類決定連接點是否匹配,而this()則通過判斷代理類是否按類型匹配指定類來決定是否和切點 匹配。兩者都僅接受類名的入參,雖然類名可以帶“+”通配符,但對於這兩個函數來說,使用與不使用+通配符,效果完全相同。

1)target()
target(M)表示如果目標類按類型匹配於M,則目標類所有方法匹配切點,我們通過一些例子理解target(M)的匹配規則:
 target(com.baobaotao.Waiter)
NaiveWaiter、NaughtyWaiter以及CuteNaiveWaiter的所有方法都匹配切點,包括那些未在Waiter接口中定義的方法,如NaiveWaiter#simle()和NaughtyWaiter#joke()方法。
 target(com.baobaotao.Waiter+)
和target(com.baobaotao.Waiter)是等價的。

2)this()
根 據Spring的官方文檔,this()函數判斷代理對象的類是否按類型匹配於指定類,如果匹配,則代理對象的所有連接點匹配切點。但通過實驗,我們發現 實際情況和文檔有出入,如我們聲明一個this(com.baobaotao.NaiveWaiter)的切點,如果不使用CGLib代理,則生成的代理 對象是Waiter類型,而非NaiveWaiter類型,這一點可以簡單地通過instanceof操作符進行判斷。但是,我們發現 NaiveWaiter中所有的方法還是被織入了增強。

在一般情況下,使用this()和target()通過定義切點,兩者是等效的:
1) target(com.baobaotao.Waiter) 等價於this(com.baobaotao.Waiter)
2) target(com.baobaotao.NaiveWaiter) 等價於 this(com.baobaotao.NaiveWaiter)
兩 者區別體現在通過引介切面產生的代理對象時的具體表現,如果我們通過本文前面的方法爲NaiveWaiter引介一個Seller接口的實現,則 this(com.baobaotao.Seller)匹配NaiveWaiter代理對象的所有方法,包括NaiverWaiter本身的 greetTo()、serverTo()方法以及通過Seller接口引入的sell()方法。而 target(com.baobaotao.Seller)不匹配通過引介切面產生的NaiveWaiter代理對象。

下面通過具體的實例來了解這一微妙的區別,EnableSellerAspect是爲NaiveWaiter添加Seller接口實現的引介切面:
package com.baobaotao.aspectj.fun;

@Aspect
public class EnableSellerAspect{
@DeclareParents(value
= "com.baobaotao.NaiveWaiter",
defaultImpl
= SmartSeller.class)
public static Seller seller;
}

TestAspect是通過判斷運行期代理對象所屬類型來定義切點的切面:
代碼清單 6 TestAspect:通過this()指定切點
package com.baobaotao.aspectj.fun;

@Aspect
public class TestAspect {
@AfterReturning(
"this(com.baobaotao.Seller)") ①後置增強,織入到任何運行期對象爲
Seller 類型的Bean中
public void thisTest(){
System.
out.println("thisTest() executed!");
}
}

在Spring中配置這兩個切面和NaiveWaiter:
<aop:aspectj-autoproxy/>
<bean id="naiveWaiter" class="com.baobaotao.NaiveWaiter" />
<bean class="com.baobaotao.aspectj.fun.EnableSellerAspect"/>
<bean class="com.baobaotao.aspectj.fun.TestAspect" />

首 先EnableSellerAspect切面爲NaiveWaiter引介Seller接口產生一個實現Seller接口的代理對 象,TestAspect在判斷出NaiveWaiter這個代理對象實現Seller接口後,就將其切面織入到這個代理對象中,所以最終 NaiveWaiter的代理對象其實共織入了兩個切面。

運行以下的測試代碼:
String configPath = "com/baobaotao/aspectj/fun/beans.xml";
ApplicationContext ctx
= new ClassPathXmlApplicationContext(configPath);
Waiter naiveWaiter
= (Waiter) ctx.getBean("naiveWaiter");
naiveWaiter.greetTo(
"John");
naiveWaiter.serveTo(
"John");
((Seller)naiveWaiter).sell(
"Beer", "John");

輸出以下的代碼:
NaiveWaiter:greet to John...
thisTest() executed
!
NaiveWaiter:serving John...
thisTest() executed
!
SmartSeller: sell Beer to John...
thisTest() executed
!

可見代理對象的3個方法都織入了代碼清單 6通過this()所定義切面。

小結
    使用@AspectJ定義切面比基於接口定義的切面更加直觀、更加簡潔,成爲Spring所推薦的切面定義方式。掌握切點表達式語法和切點函數是學習 @AspectJ的重心,我們分別對9個切點函數進行了詳細的講述。切點表達式非常靈活,擁有強大的切點表達能力,你可以使用通配符、切點函數以及切點運 算符定義切點。

Spring 2.0在AOP上花費了很大的功夫,相比於低版本的Spring,我們看到了很大的改進。在掌握低版本Spring AOP相關知識的基礎上,你會發現學習Spring 2.0 基於@AspectJ AOP的新知識不會有太多的門檻。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章