在 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
實例,ProceedingJoinPoint
是 JoinPoint
的子介面,除了可取得接入點等資訊之外,還可以控制是否進一步呼叫目標方法,如果沒有呼叫它的 proceed
方法,就等於攔截方法的呼叫請求。
在呼叫了 ProceedingJoinPoint
的 proceed
方法之後,會執行 @Before
標註的方法,接著才是目標方法,在目標方法執行過後,ProceedingJoinPoint
的 proceed
方法返回,proceed
的傳回結果,就是目標方法的傳回結果,你也可以在這邊變更傳回結果。
在 @Around
標註的方法 return
過後,接著才是執行 @After
標註的方法,如果目標方法沒有發生例外(如果有 @Around
標註的方法的話,就是該方法沒拋出例外),接著會執行 @AfterReturning
標註的方法,若要在這時取得目標方法傳回值(如果有 @Around
標註的方法的話,就是該方法的傳回值),可以透過 @AfterReturning
的 returning
屬性設置參數名稱,如此一來就會將傳回值注入。
如果目標方法沒有發生例外(如果有 @Around
標註的方法的話,就是該方法沒拋出例外),@AfterReturning
標註的方法不會被執行,而是執行 @AfterThrowing
,若要在這時取得例外(如果有 @Around
標註的方法的話,就是該方法拋出的例外),可以透過 @AfterThrowing
的 throwing
屬性設置參數名稱,如此一來就會將例外注入。
@AfterThrowing
並不能攔截例外,在執行過被標註的方法後,例外會繼續傳播,如果想要攔截例外的話,必須在 @Around
中,對 ProceedingJoinPoint
的 proceed
方法進行 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 各標註的 pointcut
與 value
屬性作用是相同的,value
是 Java 定義標註時預設就會有屬性,pointcut
是自定義的屬性。