AspectJ 切面註解中五種通知註解:@Before、@After、@AfterRunning、@AfterThrowing、@Around
要在 Spring 中宣告 AspectJ 切面, 只需要在 IOC 容器中將切面宣告為 Bean 例項. 當在 Spring IOC 容器中初始化 AspectJ 切面之後, Spring IOC 容器就會為那些與 AspectJ 切面相匹配的 Bean 建立代理。
在切面類中需要定義切面方法用於響應響應的目標方法,切面方法即為通知方法,通知方法需要用註解標識,AspectJ 支援 5 種類型的通知註解:
- @Before: 前置通知, 在方法執行之前執行
- @After: 後置通知, 在方法執行之後執行 。
- @AfterRunning: 返回通知, 在方法返回結果之後執行
- @AfterThrowing: 異常通知, 在方法丟擲異常之後
- @Around: 環繞通知, 圍繞著方法執行
下面分別舉例5中通知方法的使用
首先建立一個目標介面ArithmeticCalculator:
package lzj.com.spring.aop;
public interface ArithmeticCalculator {
int add(int i, int j);
int div(int i, int j);
}
然後建立介面的實現類ArithmeticCalculatorIml :
package lzj.com.spring.aop;
import org.springframework.stereotype.Component;
@Component ("arithmeticCalculator")
public class ArithmeticCalculatorIml implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("add->result:" + result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("div->result:" + result);
return result;
}
}
配置檔案bean-aop.xml:
<context:component-scan base-package="lzj.com.spring.aop"></context:component-scan>
建立測試類:
package lzj.com.spring.aop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-aop.xml");
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
arithmetic.add(3, 2);
arithmetic.div(4, 2);
}
}
執行結果:
add->result:5
div->result:2
上面的例子把目標類注入到IOC容器中,執行時從容器中獲取目標類的bean,然後呼叫目標方法。
下面要在目標方法的前後等執行其它操作,列印日誌,不需要改變任何目標方法,只需要增加切面類,新建切面類LogProxy,把切面類注入到IOC中,然後在切面類中定義要執行的切面方法即可。
在執行下面切面方法之前,需要先啟動五種註解,配置檔案中定義如下:
<context:component-scan base-package="lzj.com.spring.aop"></context:component-scan>
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
一、@Before前置通知
用@Before標識的方法為前置方法,在目標方法的執行之前執行,即在連線點之前進行執行。
示例如下:
package lzj.com.spring.aop;
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogProxy {
@Before("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public void beforMethod(JoinPoint point){
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("呼叫前連線點方法為:" + methodName + ",引數為:" + args);
}
}
執行測試類,輸出結果如下:
呼叫前連線點方法為:add,引數為:[3, 2]
add->result:5
呼叫前連線點方法為:div,引數為:[4, 2]
div->result:2
在目標方法add和div之前分別執行了前置通知方法。
二、@After後置通知方法
後置方法在連線點方法完成之後執行,無論連線點方法執行成功還是出現異常,都將執行後置方法。示例如下:
@Aspect
@Component
public class LogProxy {
@After(("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))"))
public void afterMethod(JoinPoint point){
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("呼叫後連線點方法為:" + methodName + ",引數為:" + args);
}
}
執行測試類,輸出結果如下:
add->result:5
呼叫後連線點方法為:add,引數為:[3, 2]
div->result:2
呼叫後連線點方法為:div,引數為:[4, 2]
發現add和div兩個連線點方法執行之後都呼叫了後置方法。如果目標連線點方法出現異常時,也會執行後置通知方法。把測試方法改成如下:
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-aop.xml");
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
arithmetic.add(3, 2);
/*被除數為0,會丟擲異常*/
arithmetic.div(4, 0);
}
執行測試方法,輸出結果如下:
add->result:5
呼叫後連線點方法為:add,引數為:[3, 2]
呼叫後連線點方法為:div,引數為:[4, 0]
Exception in thread "main" java.lang.ArithmeticException: / by zero
at lzj.com.spring.aop.ArithmeticCalculatorIml.div(ArithmeticCalculatorIml.java:17)
……
從輸出結果中可以看出,即使目標方法出現異常,後置通知方法依然執行。但後置通知拿不到目標方法執行後的結果,因為目標方法有可能出現異常。如果要拿目標方法的執行結果,要用下面的通知方法。
三、@AfterRunning返回通知方法
當連線點方法成功執行後,返回通知方法才會執行,如果連線點方法出現異常,則返回通知方法不執行。返回通知方法在目標方法執行成功後才會執行,所以,返回通知方法可以拿到目標方法(連線點方法)執行後的結果。切面類中定義返回通知方法,示例如下:
@Aspect
@Component
public class LogProxy {
/*通過returning屬性指定連線點方法返回的結果放置在result變數中,在返回通知方法中可以從result變數中獲取連線點方法的返回結果了。*/
@AfterReturning(value="execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))",
returning="result")
public void afterReturning(JoinPoint point, Object result){
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("連線點方法為:" + methodName + ",引數為:" + args + ",目標方法執行結果為:" + result);
}
}
執行測試方法,輸出結果如下:
add->result:5
連線點方法為:add,引數為:[3, 2],目標方法執行結果為:5
div->result:2
連線點方法為:div,引數為:[4, 2],目標方法執行結果為:2
當連線點方法出現異常時,不執行返回通知方法,把測試方法該為如下:
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-aop.xml");
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
arithmetic.add(3, 2);
arithmetic.div(4, 0);
}
}
執行測試方法,輸出結果如下:
add->result:5
連線點方法為:add,引數為:[3, 2],目標方法執行結果為:5
Exception in thread "main" java.lang.ArithmeticException: / by zero
……
從輸出結果可以看出,div(4,0)出現異常,因此該連線點對應的返回通知方法也不執行。
四、@AfterThrowing異常通知
異常通知方法只在連線點方法出現異常後才會執行,否則不執行。在異常通知方法中可以獲取連線點方法出現的異常。在切面類中異常通知方法,示例如下:
/*通過throwing屬性指定連線點方法出現異常資訊儲存在ex變數中,在異常通知方法中就可以從ex變數中獲取異常資訊了*/
@AfterThrowing(value="execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))",
throwing="ex")
public void afterReturning(JoinPoint point, Exception ex){
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("連線點方法為:" + methodName + ",引數為:" + args + ",異常為:" + ex);
}
測試方法為:
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-aop.xml");
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
arithmetic.add(3, 2);
arithmetic.div(4, 0);
}
}
執行測試方法,輸出結果如下:
add->result:5
連線點方法為:div,引數為:[4, 0],異常為:java.lang.ArithmeticException: / by zero
Exception in thread "main" java.lang.ArithmeticException: / by zero
從輸出結果中可以看出,add方法沒有異常,因此不執行異常通知方法,div方法出現異常,執行科異常通知方法。
上面的例子中,異常型別設定的是Exception,表示捕獲連線點方法的所有異常資訊,也可以指定捕獲指定型別的資訊,例如
@AfterThrowing(value="execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))",
throwing="ex")
/*只捕獲連線點方法中的NullPointerException 型別的異常資訊*/
public void afterReturning(JoinPoint point, NullPointerException ex){
String methodName = point.getSignature().getName();
List<Object> args = Arrays.asList(point.getArgs());
System.out.println("連線點方法為:" + methodName + ",引數為:" + args + ",異常為:" + ex);
}
五、@Around環繞通知
環繞通知方法可以包含上面四種通知方法,環繞通知的功能最全面。環繞通知需要攜帶 ProceedingJoinPoint 型別的引數,且環繞通知必須有返回值, 返回值即為目標方法的返回值。在切面類中建立環繞通知方法,示例如下:
@Around("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public Object aroundMethod(ProceedingJoinPoint pdj){
/*result為連線點的放回結果*/
Object result = null;
String methodName = pdj.getSignature().getName();
/*前置通知方法*/
System.out.println("前置通知方法>目標方法名:" + methodName + ",引數為:" + Arrays.asList(pdj.getArgs()));
/*執行目標方法*/
try {
result = pdj.proceed();
/*返回通知方法*/
System.out.println("返回通知方法>目標方法名" + methodName + ",返回結果為:" + result);
} catch (Throwable e) {
/*異常通知方法*/
System.out.println("異常通知方法>目標方法名" + methodName + ",異常為:" + e);
}
/*後置通知*/
System.out.println("後置通知方法>目標方法名" + methodName);
return result;
}
}
測試方法為:
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-aop.xml");
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
arithmetic.add(3, 2);
arithmetic.div(4, 0);
}
}
執行測試方法:
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("bean-aop.xml");
ArithmeticCalculator arithmetic = (ArithmeticCalculator) ctx.getBean("arithmeticCalculator");
arithmetic.add(3, 2);
arithmetic.div(4, 0);
}
}
執行測試方法,輸出結果:
前置通知方法>目標方法名:add,引數為:[3, 2]
add->result:5
返回通知方法>目標方法名add,返回結果為:5
後置通知方法>目標方法名add
前置通知方法>目標方法名:div,引數為:[4, 0]
異常通知方法>目標方法名div,異常為:java.lang.ArithmeticException: / by zero
後置通知方法>目標方法名div
Exception in thread "main" org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int lzj.com.spring.aop.ArithmeticCalculator.div(int,int)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:219)
at com.sun.proxy.$Proxy7.div(Unknown Source)
at lzj.com.spring.aop.Main.main(Main.java:12)
從輸出結果中可以看出,環繞通知實現了上面幾種通知的結合。
當div目標方法出現異常時,在環繞通知方法中已經用try…catch方法進行捕捉了,為什麼最後輸出結果中還出現了一個返回型別不匹配的錯誤:
Exception in thread "main" org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int lzj.com.spring.aop.ArithmeticCalculator.div(int,int)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:219)
at com.sun.proxy.$Proxy7.div(Unknown Source)
at lzj.com.spring.aop.Main.main(Main.java:12)
那是因為在環繞通知方法中開始就定義了目標方法的返回結果
Object result = null
。當目標方法出現異常時,result = pdj.proceed();
執行時出現異常,此時result中還是null,所以在環繞通知方法最後return result;
時,返回的result就是null,但是環繞通知的返回型別我們定義的是Object型別的,null不能轉化為Object型別,所以丟擲了個型別轉換的錯誤。我們可以在環繞通知方法中把異常丟擲去,即為:
@Around("execution(public int lzj.com.spring.aop.ArithmeticCalculator.*(int, int))")
public Object aroundMethod(ProceedingJoinPoint pdj){
/*result為連線點的放回結果*/
Object result = null;
String methodName = pdj.getSignature().getName();
/*前置通知方法*/
System.out.println("前置通知方法>目標方法名:" + methodName + ",引數為:" + Arrays.asList(pdj.getArgs()));
/*執行目標方法*/
try {
result = pdj.proceed();
/*返回通知方法*/
System.out.println("返回通知方法>目標方法名" + methodName + ",返回結果為:" + result);
} catch (Throwable e) {
/*異常通知方法*/
System.out.println("異常通知方法>目標方法名" + methodName + ",異常為:" + e);
/*當環繞通知方法本身還有其它異常時,非連線點方法出現的異常,此時丟擲來*/
throw new RuntimeException();
}
/*後置通知*/
System.out.println("後置通知方法>目標方法名" + methodName);
return result;
}
}
在輸出結果中會丟擲一個執行時異常java.lang.RuntimeException
插曲:不可以在執行目標方法時在定義result變數:
……
/*執行目標方法*/
try {
Object result = pdj.proceed();
……
} catch (Throwable e) {
……
}
……
return result;
這種方法是行不通的,在Object result = pdj.proceed();
中,如果pdj.proceed()
執行失敗,就會被try …catch捕獲到異常,而不會就不會執行定義result變數那一步了,即Object result
不會執行,所以在return result;
就會出現錯誤。