認識 Advice


在 Spring 中 Advice 只支援方法的織入,針對方法執行前後以及發生例外等情況,可以設置的 Advice 有 Around、Before、After、After Throwing、After Returning 等,若用標註設定,可分別使用 @Around@Before@After@AfterThrowing@AfterReturning 等。

底下的範例可用來得知這些 Advice 的執行時機:

package cc.openhome.aspect;

import java.util.Arrays;
import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LoggingAspect {
    @Around("execution(* cc.openhome.model.AccountDAO.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log(proceedingJoinPoint, "around before calling proceed()");
        Object result = proceedingJoinPoint.proceed();
        log(proceedingJoinPoint, "around after calling proceed()");
        return result;
    }

    @Before("execution(* cc.openhome.model.AccountDAO.*(..))")
    public void before(JoinPoint joinPoint) {   
        log(joinPoint, "before");
    }

    @After("execution(* cc.openhome.model.AccountDAO.*(..))")
    public void after(JoinPoint joinPoint) {
        log(joinPoint, "after");
    } 

    @AfterReturning(pointcut = "execution(* cc.openhome.model.AccountDAO.*(..))", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        log(joinPoint, String.format("afterReturning %s", result));
    }

    @AfterThrowing(pointcut="execution(* cc.openhome.model.AccountDAO.*(..))", throwing="throwable")
    public void afterThrowing(JoinPoint joinPoint, Throwable throwable) {
        log(joinPoint, String.format("afterThrowing %s", throwable));
    }

    private void log(JoinPoint joinPoint, String adviceType) {
        Object target = joinPoint.getTarget();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Logger.getLogger(target.getClass().getName()).info(String.format("%s %s.%s(%s)",
                adviceType, target.getClass().getName(), methodName, Arrays.toString(args)));
    } 
}

在沒有方法拋出例外的情況下,會有以下的結果:

11月 27, 2018 11:10:53 上午 cc.openhome.aspect.LoggingAspect log
INFO: around before calling proceed() cc.openhome.model.AccountDAOJdbcImpl.accountByUsername([caterpillar])
11月 27, 2018 11:10:53 上午 cc.openhome.aspect.LoggingAspect log
INFO: before cc.openhome.model.AccountDAOJdbcImpl.accountByUsername([caterpillar])
11月 27, 2018 11:10:53 上午 cc.openhome.aspect.LoggingAspect log
INFO: around after calling proceed() cc.openhome.model.AccountDAOJdbcImpl.accountByUsername([caterpillar])
11月 27, 2018 11:10:53 上午 cc.openhome.aspect.LoggingAspect log
INFO: after cc.openhome.model.AccountDAOJdbcImpl.accountByUsername([caterpillar])
11月 27, 2018 11:10:53 上午 cc.openhome.aspect.LoggingAspect log
INFO: afterReturning Optional[cc.openhome.model.Account@64c2b546] cc.openhome.model.AccountDAOJdbcImpl.accountByUsername([caterpillar])

可以看到的是,@Around 標註的 Advice 先被執行,被標註的方法可接受 ProceedingJoinPoint 實例,ProceedingJoinPointJoinPoint 的子介面,除了可取得接入點等資訊之外,還可以控制是否進一步呼叫目標方法,如果沒有呼叫它的 proceed 方法,就等於攔截方法的呼叫請求。

在呼叫了 ProceedingJoinPointproceed 方法之後,會執行 @Before 標註的方法,接著才是目標方法,在目標方法執行過後,ProceedingJoinPointproceed 方法返回,proceed 的傳回結果,就是目標方法的傳回結果,你也可以在這邊變更傳回結果。

@Around 標註的方法 return 過後,接著才是執行 @After 標註的方法,如果目標方法沒有發生例外(如果有 @Around 標註的方法的話,就是該方法沒拋出例外),接著會執行 @AfterReturning 標註的方法,若要在這時取得目標方法傳回值(如果有 @Around 標註的方法的話,就是該方法的傳回值),可以透過 @AfterReturningreturning 屬性設置參數名稱,如此一來就會將傳回值注入。

如果目標方法沒有發生例外(如果有 @Around 標註的方法的話,就是該方法沒拋出例外),@AfterReturning 標註的方法不會被執行,而是執行 @AfterThrowing,若要在這時取得例外(如果有 @Around 標註的方法的話,就是該方法拋出的例外),可以透過 @AfterThrowingthrowing 屬性設置參數名稱,如此一來就會將例外注入。

@AfterThrowing 並不能攔截例外,在執行過被標註的方法後,例外會繼續傳播,如果想要攔截例外的話,必須在 @Around 中,對 ProceedingJoinPointproceed 方法進行 try/catch,若有必要,也可以在捕捉到例外之後,修改或拋出其他類型的例外。

在上面的範例中,可以看到多個 Advice 設置的 Pointcut 都相同,你可以藉由 @Pointcut 來管理相同的 Pointcut,例如:

package cc.openhome.aspect;

import java.util.Arrays;
import java.util.logging.Logger;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class LoggingAspect {
    @Pointcut("execution(* cc.openhome.model.AccountDAO.*(..))")
    public void log() {}

    @Around("log()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log(proceedingJoinPoint, "around before calling proceed()");
        Object result = proceedingJoinPoint.proceed();
        log(proceedingJoinPoint, "around after calling proceed()");
        return result;
    }

    @Before("log()")
    public void before(JoinPoint joinPoint) {   
        log(joinPoint, "before");
    }

    @After("log()")
    public void after(JoinPoint joinPoint) {
        log(joinPoint, "after");
    } 

    @AfterReturning(pointcut = "log()", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
        log(joinPoint, String.format("afterReturning %s", result));
    }

    @AfterThrowing(pointcut="log()", throwing="throwable")
    public void afterThrowing(JoinPoint joinPoint, Throwable throwable) {
        log(joinPoint, String.format("afterThrowing %s", throwable));
    }

    private void log(JoinPoint joinPoint, String adviceType) {
        Object target = joinPoint.getTarget();
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        Logger.getLogger(target.getClass().getName()).info(String.format("%s %s.%s(%s)",
                adviceType, target.getClass().getName(), methodName, Arrays.toString(args)));
    } 
}

@Pointcut 用來標註在某個空方法上,該方法的名稱可用來設定 Advice 的 pointcut 屬性,Advice 各標註的 pointcutvalue 屬性作用是相同的,value 是 Java 定義標註時預設就會有屬性,pointcut 是自定義的屬性。