通知參數
前邊章節已經介紹了聲明通知,但如果想擷取被被通知方法參數并傳遞給通知方法,該如何實作呢?接下來我們将介紹兩種擷取通知參數的方式。
- 使用JoinPoint擷取:Spring AOP提供使用org.aspectj.lang.JoinPoint類型擷取連接配接點資料,任何通知方法的第一個參數都可以是JoinPoint(環繞通知是ProceedingJoinPoint,JoinPoint子類),當然第一個參數位置也可以是JoinPoint.StaticPart類型,這個隻傳回連接配接點的靜态部分。
1) JoinPoint:提供通路目前被通知方法的目标對象、代理對象、方法參數等資料:
java代碼: Java代碼
- package org.aspectj.lang;
- import org.aspectj.lang.reflect.SourceLocation;
- public interface JoinPoint {
- String toString(); //連接配接點所在位置的相關資訊
- String toShortString(); //連接配接點所在位置的簡短相關資訊
- String toLongString(); //連接配接點所在位置的全部相關資訊
- Object getThis(); //傳回AOP代理對象
- Object getTarget(); //傳回目标對象
- Object[] getArgs(); //傳回被通知方法參數清單
- Signature getSignature(); //傳回目前連接配接點簽名
- SourceLocation getSourceLocation();//傳回連接配接點方法所在類檔案中的位置
- String getKind(); //連接配接點類型
- StaticPart getStaticPart(); //傳回連接配接點靜态部分
- }
2)ProceedingJoinPoint:用于環繞通知,使用proceed()方法來執行目标方法:
java代碼: Java代碼
- public interface ProceedingJoinPoint extends JoinPoint {
- public Object proceed() throws Throwable;
- public Object proceed(Object[] args) throws Throwable;
- }
3) JoinPoint.StaticPart:提供通路連接配接點的靜态部分,如被通知方法簽名、連接配接點類型等:
java代碼: Java代碼
- public interface StaticPart {
- Signature getSignature(); //傳回目前連接配接點簽名
- String getKind(); //連接配接點類型
- int getId(); //唯一辨別
- String toString(); //連接配接點所在位置的相關資訊
- String toShortString(); //連接配接點所在位置的簡短相關資訊
- String toLongString(); //連接配接點所在位置的全部相關資訊
- }
使用如下方式在通知方法上聲明,必須是在第一個參數,然後使用jp.getArgs()就能擷取到被通知方法參數:
java代碼: Java代碼
- @Before(value="execution(* sayBefore(*))")
- public void before(JoinPoint jp) {}
- @Before(value="execution(* sayBefore(*))")
- public void before(JoinPoint.StaticPart jp) {}
- 自動擷取:通過切入點表達式可以将相應的參數自動傳遞給通知方法,例如前邊章節講過的傳回值和異常是如何傳遞給通知方法的。
在Spring AOP中,除了execution和bean訓示符不能傳遞參數給通知方法,其他訓示符都可以将比對的相應參數或對象自動傳遞給通知方法。
java代碼: Java代碼
- @Before(value="execution(* test(*)) && args(param)", argNames="param")
- public void before1(String param) {
- System.out.println("===param:" + param);
- }
切入點表達式execution(* test(*)) && args(param) :
1)首先execution(* test(*))比對任何方法名為test,且有一個任何類型的參數;
2)args(param)将首先查找通知方法上同名的參數,并在方法執行時(運作時)比對傳入的參數是使用該同名參數類型,即java.lang.String;如果比對将把該被通知參數傳遞給通知方法上同名參數。
其他訓示符(除了execution和bean訓示符)都可以使用這種方式進行參數綁定。
在此有一個問題,即前邊提到的類似于【3.1.2構造器注入】中的參數名注入限制:在class檔案中沒生成變量調試資訊是擷取不到方法參數名字的。
是以我們可以使用政策來确定參數名:
1、如果我們通過“argNames”屬性指定了參數名,那麼就是要我們指定的;
java代碼: Java代碼
- @Before(value=" args(param)", argNames="param") //明确指定了
- public void before1(String param) {
- System.out.println("===param:" + param);
- }
2、如果第一個參數類型是JoinPoint、ProceedingJoinPoint或JoinPoint.StaticPart類型,應該從“argNames”屬性省略掉該參數名(可選,寫上也對),這些類型對象會自動傳入的,但必須作為第一個參數;
java代碼: Java代碼
- @Before(value=" args(param)", argNames="param") //明确指定了
- public void before1(JoinPoint jp, String param) {
- System.out.println("===param:" + param);
- }
3、如果“class檔案中含有變量調試資訊”将使用這些方法簽名中的參數名來确定參數名;
java代碼: Java代碼
- @Before(value=" args(param)") //不需要argNames了
- public void before1(JoinPoint jp, String param) {
- System.out.println("===param:" + param);
- }
4、如果沒有“class檔案中含有變量調試資訊”,将嘗試自己的參數比對算法,如果發現參數綁定有二義性将抛出AmbiguousBindingException異常;對于隻有一個綁定變量的切入點表達式,而通知方法隻接受一個參數,說明綁定參數是明确的,進而能配對成功。
java代碼: Java代碼
- @Before(value=" args(param)")
- public void before1(JoinPoint jp, String param) {
- System.out.println("===param:" + param);
- }
5、以上政策失敗将抛出IllegalArgumentException。
接下來讓我們示例一下組合情況吧:
java代碼: Java代碼
- @Before(args(param) && target(bean) && @annotation(secure)",
- argNames="jp,param,bean,secure")
- public void before5(JoinPoint jp, String param,
- IPointcutService pointcutService, Secure secure) {
- ……
- }
該示例的執行步驟如圖6-5所示。
圖6-5 參數自動擷取流程
除了上邊介紹的普通方式,也可以對使用命名切入點自動擷取參數:
java代碼: Java代碼
- @Pointcut(value="args(param)", argNames="param")
- private void pointcut1(String param){}
- @Pointcut(value="@annotation(secure)", argNames="secure")
- private void pointcut2(Secure secure){}
- @Before(value = "pointcut1(param) && pointcut2(secure)",
- argNames="param, secure")
- public void before6(JoinPoint jp, String param, Secure secure) {
- ……
- }
自此給通知傳遞參數已經介紹完了,示例代碼在cn.javass.spring.chapter6.ParameterTest檔案中。
通知順序
如果我們有多個通知想要在同一連接配接點執行,那執行順序如何确定呢?Spring AOP使用AspectJ的優先級規則來确定通知執行順序。總共有兩種情況:同一切面中通知執行順序、不同切面中的通知執行順序。
首先讓我們看下
1) 同一切面中通知執行順序:如圖6-6所示。
圖6-6 同一切面中的通知執行順序
而如果在同一切面中定義兩個相同類型通知(如同是前置通知或環繞通知(proceed之前))并在同一連接配接點執行時,其執行順序是未知的,如果确實需要指定執行順序需要将通知重構到兩個切面,然後定義切面的執行順序。
java代碼: Java代碼
- 錯誤“Advice precedence circularity error”:說明AspectJ無法決定通知的執行順序,隻要将通知方法分類并按照順序排列即可解決。
2)不同切面中的通知執行順序:當定義在不同切面的相同類型的通知需要在同一個連接配接點執行,如果沒指定切面的執行順序,這兩個通知的執行順序将是未知的。
如果需要他們順序執行,可以通過指定切面的優先級來控制通知的執行順序。
Spring中可以通過在切面實作類上實作org.springframework.core.Ordered接口或使用Order注解來指定切面優先級。在多個切面中,Ordered.getValue()方法傳回值(或者注解值)較小值的那個切面擁有較高優先級,如圖6-7所示。
圖6-7 兩個切面指定了優先級
對于@AspectJ風格和注解風格可分别用以下形式指定優先級:
在此我們不推薦使用實作Ordered接口方法,是以沒介紹,示例代碼在cn.javass.spring.chapter6. OrderAopTest檔案中。
切面執行個體化模型
所謂切面執行個體化模型指何時執行個體化切面。
Spring AOP支援AspectJ的singleton、perthis、pertarget執行個體化模型(目前不支援percflow、percflowbelow 和pertypewithin)。
- singleton:即切面隻會有一個執行個體;
- perthis:每個切入點表達式比對的連接配接點對應的AOP對象都會建立一個新切面執行個體;
- pertarget:每個切入點表達式比對的連接配接點對應的目标對象都會建立一個新的切面執行個體;
預設是singleton執行個體化模型,Schema風格隻支援singleton執行個體化模型,而@AspectJ風格支援這三種執行個體化模型。
singleton:使用@Aspect()指定,即預設就是單例執行個體化模式,在此就不示範示例了。
perthis:每個切入點表達式比對的連接配接點對應的AOP對象都會建立一個新的切面執行個體,使用@Aspect("perthis(切入點表達式)")指定切入點表達式;
如@Aspect("perthis(this(cn.javass.spring.chapter6.service.IIntroductionService))")将對每個比對“this(cn.javass.spring.chapter6.service.IIntroductionService)”切入點表達式的AOP代理對象建立一個切面執行個體,注意“IIntroductionService”可能是引入接口。
pertarget:每個切入點表達式比對的連接配接點對應的目标對象都會建立一個新的切面執行個體,使用@Aspect("pertarget(切入點表達式)")指定切入點表達式;
如@Aspect("pertarget(target(cn.javass.spring.chapter6. service.IPointcutService))")将對每個比對“target(cn.javass.spring.chapter6.service. IPointcutService)”切入點表達式的目标對象建立一個切面,注意“IPointcutService”不可能是引入接口。
在進行切面定義時必須将切面scope定義為“prototype”,如“<bean class="……Aspect" scope="prototype"/>”,否則不能為每個比對的連接配接點的目标對象或AOP代理對象建立一個切面。
示例請參考cn.javass.spring.chapter6. InstanceModelTest。
代理機制
Spring AOP通過代理模式實作,目前支援兩種代理:JDK動态代理、CGLIB代理來建立AOP代理,Spring建議優先使用JDK動态代理。
- JDK動态代理:使用java.lang.reflect.Proxy動态代理實作,即提取目标對象的接口,然後對接口建立AOP代理。
- CGLIB代理:CGLIB代理不僅能進行接口代理,也能進行類代理,CGLIB代理需要注意以下問題:
不能通知final方法,因為final方法不能被覆寫(CGLIB通過生成子類來建立代理)。
會産生兩次構造器調用,第一次是目标類的構造器調用,第二次是CGLIB生成的代理類的構造器調用。如果需要CGLIB代理方法,請確定兩次構造器調用不影響應用。
Spring AOP預設首先使用JDK動态代理來代理目标對象,如果目标對象沒有實作任何接口将使用CGLIB代理,如果需要強制使用CGLIB代理,請使用如下方式指定:
對于Schema風格配置切面使用如下方式來指定使用CGLIB代理:
java代碼: Java代碼
- <aop:config proxy-target-class="true">
- </aop:config>
而如果使用@AspectJ風格使用如下方式來指定使用CGLIB代理:
java代碼: Java代碼
- <aop:aspectj-autoproxy proxy-target-class="true"/>