1. 程式人生 > >AspectJ 切面註解中五種通知註解:@Before、@After、@AfterRunning、@AfterThrowing、@Around

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;就會出現錯誤。