一. AOP標籤介紹
基於Schema的AOP從Spring2.0之後通過“aop”命名空間來定義切面、切入點及聲明通知。在Spring配置文件中,所以AOP相關定義必須放在<aop:config>
標籤下,該標籤下可以有<aop:pointcut>
、<aop:advisor>
、<aop:aspect>
標籤, 配置順序不可變 。
<aop:pointcut>
:用來定義切入點,該切入點可以重用;<aop:advisor>
:用來定義只有一個通知和一個切入點的切面;<aop:aspect>
:用來定義切面,該切面可以包含多個切入點和通知,而且標籤內部的通知和切入點定義是無序的;和advisor的區別就在此,advisor只包含一個通知和一個切入點。
Spring AOP 切面申明的兩種方式:
1.基於Schema的AOP申明使用 <aop:aspect>
配置既可,也就是本文所講的內容.
2.基於@AspectJ風格的切面聲明,也就是顯示的配置<aop:aspectj-autoproxy />
,然後在代碼中就可以識別AspectJ的註解了(比如@Aspect,@Pointcut,@Before…)(在接下里的文章中會介紹)
二. 聲明切面
切面就是包含切入點和通知的對象,在Spring容器中將被定義爲一個Bean,Schema方式的切面需要一個切面支持Bean,該支持Bean的字段和方法提供了切面的狀態和行爲信息,並通過配置方式來指定切入點和通知實現。
切面使用<aop:aspect>
標籤指定,ref屬性用來引用切面支持Bean。
切面支持Bean“aspectSupportBean”跟普通Bean完全一樣使用,切面使用“ref”屬性引用它。
三. 聲明切入點
切入點在Spring中也是一個Bean,Bean定義方式可以有很三種方式:
1)在aop:config標籤下使用aop:pointcut聲明一個切入點Bean,該切入點可以被多個切面使用,對於需要共享使用的切入點最好使用該方式,該切入點使用id屬性指定Bean名字,在通知定義時使用pointcut-ref屬性通過該id引用切入點,expression屬性指定切入點表達式:
<bean id="aspect" class="com.yveshe.aop.Audience"/>
<aop:config>
<aop:pointcut id="pointcut" expression="execution(** com.yveshe.Performance.perform(..))"/>
<aop:aspect ref="aspect">
<aop:before pointcut-ref="pointcut" method="beforePerfomanceNotice" />
</aop:aspect>
</aop:config>
2)在aop:aspect標籤下使用aop:pointcut聲明一個切入點Bean,該切入點可以被多個切面使用,但一般該切入點只被該切面使用,當然也可以被其他切面使用,但最好不要那樣使用,該切入點使用id屬性指定Bean名字,在通知定義時使用pointcut-ref屬性通過該id引用切入點,expression屬性指定切入點表達式:
<bean id="aspect" class="com.yveshe.aop.Audience"/>
<aop:config>
<aop:aspect ref="aspect">
<aop:pointcut id="pointcut" expression="execution(** com.yveshe.Performance.perform(..)) "/>
<aop:before pointcut-ref="pointcut" method="beforePerfomanceNotice" />
</aop:aspect>
</aop:config>
3)匿名切入點Bean,可以在聲明通知時通過pointcut屬性指定切入點表達式,該切入點是匿名切入點,只被該通知使用:
<bean id="aspect" class="com.yveshe.aop.Audience"/>
<aop:config>
<aop:aspect ref="aspect">
<aop:pointcut id="pointcut" expression="execution(** com.yveshe.Performance.perform(..)) "/>
<aop:before pointcut-ref="pointcut" method="beforePerfomanceNotice" />
</aop:aspect>
</aop:config>
<aop:config>
<aop:aspect ref="aspectSupportBean">
<aop:after pointcut="execution(* cn.javass..*.*(..))" method="afterFinallyAdvice"/>
</aop:aspect>
</aop:config>
四. 聲明通知
基於Schema方式支持前邊介紹的5中通知類型:
- 前置通知(Before) : 在目標方法被調用之前調用通知功能;
- 後置通知(After) : 在目標方法完成之後調用通知, 此時不會關心方法的輸出是什麼;
- 返回通知(After-returning) : 在目標方法成功執行之後調用通知;
- 異常通知(After-throwing) : 在目標方法拋出異常後調用通知;
- 環繞通知(Around) : 通知包裹了被通知的方法, 在被通知的方法調用之前和調用之後執行自定義的行爲。
1. 前置通知
在切入點選擇的方法之前執行,通過aop:aspect標籤下的aop:before標籤聲明:
<aop:before pointcut="切入點表達式" pointcut-ref="切入點Bean引用"
method="前置通知實現方法名"
arg-names="前置通知實現方法參數列表參數名字"/>
pointcut和pointcut-ref:二者選一,指定切入點;
**method:**指定前置通知實現方法名,如果是多態需要加上參數類型,多個用“,”隔開,如beforeAdvice(java.lang.String);
**arg-names:**指定通知實現方法的參數名字,多個用“,”分隔,可選
首先在cn.javass.spring.chapter6.service.IhelloWorldService定義一個測試方法:
public void sayBefore(String param);
其次在cn.javass.spring.chapter6.service.impl. HelloWorldService定義實現
@Override
public void sayBefore(String param) {
System.out.println("============say " + param);
}
第三在cn.javass.spring.chapter6.aop. HelloWorldAspect定義通知實現:
public void beforeAdvice(String param) {
System.out.println("===========before advice param:" + param);
}
最後在chapter6/advice.xml配置文件中進行如下配置:
<bean id="helloWorldService" class="cn.javass.spring.chapter6.service.impl.HelloWorldService"/>
<bean id="aspect" class="cn.javass.spring.chapter6.aop.HelloWorldAspect"/>
<aop:config>
<aop:aspect ref="aspect">
<aop:before pointcut="execution(* cn.javass..*.sayBefore(..)) and args(param)"
method="beforeAdvice(java.lang.String)"
arg-names="param"/>
</aop:aspect>
</aop:config>
測試代碼cn.javass.spring.chapter6.AopTest:
@Test
public void testSchemaBeforeAdvice(){
System.out.println("======================================");
ApplicationContext ctx = new ClassPathXmlApplicationContext("chapter6/advice.xml");
IHelloWorldService helloworldService = ctx.getBean("helloWorldService", IHelloWorldService.class);
helloworldService.sayBefore("before");
System.out.println("======================================");
}
將輸入:
==========================================
===========before advice param:before
============say before
==========================================
分析一下吧:
1)切入點匹配: 在配置中使用“execution(* cn.javass…*.sayBefore(…)) ”匹配目標方法sayBefore,且使用“args(param)”匹配目標方法只有一個參數且傳入的參數類型爲通知實現方法中同名的參數類型;
2)目標方法定義: 使用method=" beforeAdvice(java.lang.String) “指定前置通知實現方法,且該通知有一個參數類型爲java.lang.String參數;
3)目標方法參數命名: 其中使用arg-names=” param "指定通知實現方法參數名爲“param”,切入點中使用“args(param)”匹配的目標方法參數將自動傳遞給通知實現方法同名參數。
2. 後置返回通知
在切入點選擇的方法正常返回時執行,通過aop:aspect標籤下的aop:after-returning標籤聲明:
<aop:after-returning pointcut="切入點表達式" pointcut-ref="切入點Bean引用"
method="後置返回通知實現方法名"
arg-names="後置返回通知實現方法參數列表參數名字"
returning="返回值對應的後置返回通知實現方法參數名"
/>
pointcut和pointcut-ref: 同前置通知同義;
method: 同前置通知同義;
arg-names: 同前置通知同義;
returning: 定義一個名字,該名字用於匹配通知實現方法的一個參數名,當目標方法執行正常返回後,將把目標方法返回值傳給通知方法;returning限定了只有目標方法返回值匹配與通知方法相應參數類型時才能執行後置返回通知,否則不執行,對於returning對應的通知方法參數爲Object類型將匹配任何目標返回值。
首先在cn.javass.spring.chapter6.service.IhelloWorldService定義一個測試方法:
public boolean sayAfterReturning();
其次在cn.javass.spring.chapter6.service.impl. HelloWorldService定義實現
@Override
public boolean sayAfterReturning() {
System.out.println("============after returning");
return true;
}
第三在cn.javass.spring.chapter6.aop. HelloWorldAspect定義通知實現:
public void afterReturningAdvice(Object retVal) {
System.out.println("===========after returning advice retVal:" + retVal);
}
最後在chapter6/advice.xml配置文件中接着前置通知配置的例子添加如下配置:
<aop:after-returning pointcut="execution(* cn.javass..*.sayAfterReturning(..))"
method="afterReturningAdvice"
arg-names="retVal"
returning="retVal"/>
測試代碼cn.javass.spring.chapter6.AopTest:
@Test
public void testSchemaAfterReturningAdvice() {
System.out.println("======================================");
ApplicationContext ctx = new ClassPathXmlApplicationContext("chapter6/advice.xml");
IHelloWorldService helloworldService = ctx.getBean("helloWorldService", IHelloWorldService.class);
helloworldService.sayAfterReturning();
System.out.println("======================================");
}
將輸入:
======================================
============after returning
===========after returning advice retVal:true
======================================
分析一下吧:
1)切入點匹配: 在配置中使用“execution(* cn.javass…*.sayAfterReturning(…)) ”匹配目標方法sayAfterReturning,該方法返回true;
2)目標方法定義: 使用method="afterReturningAdvice"指定後置返回通知實現方法;
3)目標方法參數命名: 其中使用arg-names="retVal"指定通知實現方法參數名爲“retVal”;
4)返回值命名: returning="retVal"用於將目標返回值賦值給通知實現方法參數名爲“retVal”的參數上。
3. 後置異常通知
在切入點選擇的方法拋出異常時執行,通過aop:aspect標籤下的aop:after-throwing標籤聲明:
<aop:after-throwing pointcut="切入點表達式" pointcut-ref="切入點Bean引用"
method="後置異常通知實現方法名"
arg-names="後置異常通知實現方法參數列表參數名字"
throwing="將拋出的異常賦值給的通知實現方法參數名"/>
pointcut和pointcut-ref: 同前置通知同義;
method: 同前置通知同義;
arg-names: 同前置通知同義;
throwing: 定義一個名字,該名字用於匹配通知實現方法的一個參數名,當目標方法拋出異常返回後,將把目標方法拋出的異常傳給通知方法;throwing限定了只有目標方法拋出的異常匹配與通知方法相應參數異常類型時才能執行後置異常通知,否則不執行,對於throwing對應的通知方法參數爲Throwable類型將匹配任何異常。
首先在cn.javass.spring.chapter6.service.IhelloWorldService定義一個測試方法:
public void sayAfterThrowing();
其次在cn.javass.spring.chapter6.service.impl. HelloWorldService定義實現
@Override
public void sayAfterThrowing() {
System.out.println("============before throwing");
throw new RuntimeException();
}
第三在cn.javass.spring.chapter6.aop. HelloWorldAspect定義通知實現:
public void afterThrowingAdvice(Exception exception) {
System.out.println("===========after throwing advice exception:" + exception);
}
最後在chapter6/advice.xml配置文件中接着前置通知配置的例子添加如下配置:
<aop:after-throwing pointcut="execution(* cn.javass..*.sayAfterThrowing(..))"
method="afterThrowingAdvice"
arg-names="exception"
throwing="exception"/>
測試代碼cn.javass.spring.chapter6.AopTest:
@Test(expected = RuntimeException.class)
public void testSchemaAfterThrowingAdvice() {
System.out.println("======================================");
ApplicationContext ctx = new ClassPathXmlApplicationContext("chapter6/advice.xml");
IHelloWorldService helloworldService = ctx.getBean("helloWorldService", IHelloWorldService.class);
helloworldService.sayAfterThrowing();
System.out.println("======================================");
}
將輸入:
======================================
============before throwing
===========after throwing advice exception:java.lang.RuntimeException
======================================
分析一下吧:
1)切入點匹配: 在配置中使用“execution(* cn.javass…*.sayAfterThrowing(…))”匹配目標方法sayAfterThrowing,該方法將拋出RuntimeException異常;
2)目標方法定義: 使用method="afterThrowingAdvice"指定後置異常通知實現方法;
3)目標方法參數命名: 其中使用arg-names="exception"指定通知實現方法參數名爲“exception”;
4)異常命名: returning="exception"用於將目標方法拋出的異常賦值給通知實現方法參數名爲“exception”的參數上。
4. 後置最終通知
在切入點選擇的方法返回時執行,不管是正常返回還是拋出異常都執行,通過aop:aspect標籤下的<aop:after >標籤聲明:
<aop:after pointcut="切入點表達式" pointcut-ref="切入點Bean引用"
method="後置最終通知實現方法名"
arg-names="後置最終通知實現方法參數列表參數名字"/>
**pointcut和pointcut-ref:**同前置通知同義;
**method:**同前置通知同義;
**arg-names:**同前置通知同義;
首先在cn.javass.spring.chapter6.service.IhelloWorldService定義一個測試方法:
public boolean sayAfterFinally();
其次在cn.javass.spring.chapter6.service.impl. HelloWorldService定義實現
@Override
public boolean sayAfterFinally() {
System.out.println("============before finally");
throw new RuntimeException();
}
第三在cn.javass.spring.chapter6.aop. HelloWorldAspect定義通知實現:
public void afterFinallyAdvice() {
System.out.println("===========after finally advice");
}
最後在chapter6/advice.xml配置文件中接着前置通知配置的例子添加如下配置:
<aop:after pointcut="execution(* cn.javass..*.sayAfterFinally(..))"
method="afterFinallyAdvice"/>
測試代碼cn.javass.spring.chapter6.AopTest:
@Test(expected = RuntimeException.class)
public void testSchemaAfterFinallyAdvice() {
System.out.println("======================================");
ApplicationContext ctx = new ClassPathXmlApplicationContext("chapter6/advice.xml");
IHelloWorldService helloworldService = ctx.getBean("helloWorldService", IHelloWorldService.class);
helloworldService.sayAfterFinally();
System.out.println("======================================");
}
將輸入:
======================================
============before finally
===========after finally advice
======================================
分析一下吧:
1)切入點匹配: 在配置中使用“execution(* cn.javass…*.sayAfterFinally(…))”匹配目標方法sayAfterFinally,該方法將拋出RuntimeException異常;
2)目標方法定義: 使用method=" afterFinallyAdvice "指定後置最終通知實現方法。
5. 環繞通知
環繞着在切入點選擇的連接點處的方法所執行的通知,環繞通知非常強大,可以決定目標方法是否執行,什麼時候執行,執行時是否需要替換方法參數,執行完畢是否需要替換返回值,可通過aop:aspect標籤下的<aop:around >標籤聲明:
<aop:around pointcut="切入點表達式" pointcut-ref="切入點Bean引用"
method="後置最終通知實現方法名"
arg-names="後置最終通知實現方法參數列表參數名字"/>
pointcut和pointcut-ref: 同前置通知同義;
method: 同前置通知同義;
arg-names: 同前置通知同義;
環繞通知第一個參數必須是org.aspectj.lang.ProceedingJoinPoint類型,在通知實現方法內部使用ProceedingJoinPoint的proceed()方法使目標方法執行,proceed 方法可以傳入可選的Object[]數組,該數組的值將被作爲目標方法執行時的參數。
首先在cn.javass.spring.chapter6.service.IhelloWorldService定義一個測試方法:
public void sayAround(String param);
其次在cn.javass.spring.chapter6.service.impl. HelloWorldService定義實現
@Override
public void sayAround(String param) {
System.out.println("============around param:" + param);
}
第三在cn.javass.spring.chapter6.aop. HelloWorldAspect定義通知實現:
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("===========around before advice");
Object retVal = pjp.proceed(new Object[] {"replace"});
System.out.println("===========around after advice");
return retVal;
}
最後在chapter6/advice.xml配置文件中接着前置通知配置的例子添加如下配置:
<aop:around pointcut="execution(* cn.javass..*.sayAround(..))"
method="aroundAdvice"/>
測試代碼cn.javass.spring.chapter6.AopTest:
@Test
public void testSchemaAroundAdvice() {
System.out.println("======================================");
ApplicationContext ctx = new ClassPathXmlApplicationContext("chapter6/advice.xml");
IHelloWorldService helloworldService =
ctx.getBean("helloWorldService", IHelloWorldService.class);
helloworldService.sayAround("haha");
System.out.println("======================================");
}
將輸入:
======================================
===========around before advice
============around param:replace
===========around after advice
======================================
分析一下吧:
1)切入點匹配: 在配置中使用“execution(* cn.javass…*.sayAround(…))”匹配目標方法sayAround;
2)目標方法定義: 使用method="aroundAdvice"指定環繞通知實現方法,在該實現中,第一個方法參數爲pjp,類型爲ProceedingJoinPoint,其中“Object retVal = pjp.proceed(new Object[] {“replace”});”,用於執行目標方法,且目標方法參數被“new Object[] {“replace”}”替換,最後返回“retVal ”返回值。
3)測試: 我們使用“helloworldService.sayAround(“haha”);”傳入參數爲“haha”,但最終輸出爲“replace”,說明參數被替換了。
五. 引入
Spring引入允許爲目標對象引入新的接口,通過在< aop:aspect>標籤內使用< aop:declare-parents>標籤進行引入,定義方式如下:
<aop:declare-parents
types-matching="AspectJ語法類型表達式"
implement-interface=引入的接口"
default-impl="引入接口的默認實現"
delegate-ref="引入接口的默認實現Bean引用"/>
types-matching: 匹配需要引入接口的目標對象的AspectJ語法類型表達式;
implement-interface: 定義需要引入的接口;
default-impl和delegate-ref: 定義引入接口的默認實現,二者選一,default-impl是接口的默認實現類全限定名,而delegate-ref是默認的實現的委託Bean名;
測試例子參考: Spring| AOP之引入(Introductions)
六. Advisor
Advisor表示只有一個通知和一個切入點的切面,由於Spring AOP都是基於AOP聯盟的攔截器模型的環繞通知的,所以引入Advisor來支持各種通知類型(如前置通知等5種),Advisor概念來自於Spring1.2對AOP的支持,在AspectJ中沒有相應的概念對應。
Advisor可以使用aop:config標籤下的aop:advisor標籤定義:
<aop:advisor pointcut="切入點表達式" pointcut-ref="切入點Bean引用"
advice-ref="通知API實現引用"/>
pointcut和pointcut-ref: 二者選一,指定切入點表達式;
advice-ref: 引用通知API實現Bean,如前置通知接口爲MethodBeforeAdvice;
接下來讓我們看一下示例吧:
首先在cn.javass.spring.chapter6.service.IhelloWorldService定義一個測試方法:
public void sayAdvisorBefore(String param);
其次在cn.javass.spring.chapter6.service.impl. HelloWorldService定義實現
@Override
public void sayAdvisorBefore(String param) {
System.out.println("============say " + param);
}
第三定義前置通知API實現:
package cn.javass.spring.chapter6.aop;
import java.lang.reflect.Method;
import org.springframework.aop.MethodBeforeAdvice;
public class BeforeAdviceImpl implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("===========before advice");
}
}
在chapter6/advice.xml配置文件中先添加通知實現Bean定義:
<bean id="beforeAdvice" class="cn.javass.spring.chapter6.aop.BeforeAdviceImpl"/>
然後在aop:config標籤下,添加Advisor定義,添加時注意順序:
<aop:advisor pointcut="execution(* cn.javass..*.sayAdvisorBefore(..))"
advice-ref="beforeAdvice"/>
測試代碼cn.javass.spring.chapter6.AopTest:
@Test
public void testSchemaAdvisor() {
System.out.println("======================================");
ApplicationContext ctx = new ClassPathXmlApplicationContext("chapter6/advice.xml");
IHelloWorldService helloworldService =
ctx.getBean("helloWorldService", IHelloWorldService.class);
helloworldService.sayAdvisorBefore("haha");
System.out.println("======================================");
}
將輸入:
======================================
===========before advice
============say haha
======================================
在此我們只介紹了前置通知API,其他類型的在後邊章節介紹。
不推薦使用Advisor,除了在進行事務控制的情況下,其他情況一般不推薦使用該方式,該方式屬於侵入式設計,必須實現通知API。