SpringBoot系列-AOP 面向切面

基本概念

  • Advice(通知、切面): 某個連接點所採用的處理邏輯,也就是向連接點注入的代碼, AOP在特定的切入點上執行的增強處理。
    • @Before: 標識一個前置增強方法,相當於BeforeAdvice的功能。
    • @After: final增強,不管是拋出異常或者正常退出都會執行。
    • @AfterReturning: 後置增強,似於AfterReturningAdvice, 方法正常退出時執行。
    • @AfterThrowing: 異常拋出增強,相當於ThrowsAdvice。
    • @Around: 環繞增強,相當於org.aopalliance.intercept.MethodInterceptor。
  • JointPoint(連接點):程序運行中的某個階段點,比如方法的調用、異常的拋出等。
  • Pointcut(切入點): JoinPoint的集合,是程序中需要注入Advice的位置的集合,指明Advice要在什麼樣的條件下才能被觸發,在程序中主要體現爲書寫切入點表達式。
  • Advisor(增強): PointCut和Advice的綜合體,完整描述了一個advice將會在pointcut所定義的位置被觸發。
  • @Aspect(切面): 通常是一個類的註解,類中可以定義切入點和通知。
  • AOP Proxy:AOP框架創建的對象,代理就是目標對象的加強。Spring中的AOP代理可以使JDK動態代理,也可以是CGLIB代理,前者基於接口,後者基於子類。

 

Pointcut

表示式(expression)和簽名(signature)

// Pointcut表示式
@Pointcut("execution(* com.jaemon.controller.*Controller.*(..))")
// Point簽名
private void log(){} 

由下列方式來定義或者通過 &&、 ||、 !、 的方式進行組合:

  • execution:用於匹配方法執行的連接點;
  • within:用於匹配指定類型內的方法執行;
  • this:用於匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配;
  • target:用於匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配;
  • args:用於匹配當前執行的方法傳入的參數爲指定類型的執行方法;
  • @within:用於匹配所以持有指定註解類型內的方法;
  • @target:用於匹配當前目標對象類型的執行方法,其中目標對象持有指定的註解;
  • @args:用於匹配當前執行的方法傳入的參數持有指定註解的執行;
  • @annotation:用於匹配當前執行方法持有指定註解的方法;

 

格式

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?) 

其中後面跟着“?”的是可選項

括號中各個pattern分別表示:

  • 修飾符匹配(modifier-pattern?)
  • 返回值匹配(ret-type-pattern): 可以爲*表示任何返回值, 全路徑的類名等
  • 類路徑匹配(declaring-type-pattern?)
  • 方法名匹配(name-pattern):可以指定方法名 或者 代表所有, set 代表以set開頭的所有方法
  • 參數匹配((param-pattern)):可以指定具體的參數類型。多個參數間用“,”隔開,各個參數也可以用"*" 來表示匹配任意類型的參數,"…"表示零個或多個任意參數

eg. (String)表示匹配一個String參數的方法;(*,String) 表示匹配有兩個參數的方法,第一個參數可以是任意類型,而第二個參數是String類型。

  • 異常類型匹配(throws-pattern?)

舉例

  • 任意公共方法的執行:execution(public * *(…))
  • 任何一個以“set”開始的方法的執行:execution(* set*(…))
  • AccountService 接口的任意方法的執行:execution(* com.xyz.service.AccountService.*(…))
  • 定義在service包裏的任意方法的執行: execution(* com.xyz.service..(…))
  • 定義在service包和所有子包裏的任意類的任意方法的執行:execution(* com.xyz.service….(…))
  • 第一個表示匹配任意的方法返回值, …(兩個點)表示零個或多個,第一個…表示service包及其子包,第二個表示所有類, 第三個*表示所有方法,第二個…表示方法的任意參數個數
  • 定義在pointcutexp包和所有子包裏的JoinPointObjP2類的任意方法的執行:execution(* com.test.spring.aop.pointcutexp…JoinPointObjP2.*(…))")
  • pointcutexp包裏的任意類: within(com.test.spring.aop.pointcutexp.*)
  • pointcutexp包和所有子包裏的任意類:within(com.test.spring.aop.pointcutexp…*)
  • 實現了Intf接口的所有類,如果Intf不是接口,限定Intf單個類:this(com.test.spring.aop.pointcutexp.Intf)
    當一個實現了接口的類被AOP的時候,用getBean方法必須cast爲接口類型,不能爲該類的類型
  • 帶有@Transactional標註的所有類的任意方法:
    • @within(org.springframework.transaction.annotation.Transactional)
    • @target(org.springframework.transaction.annotation.Transactional)
  • 帶有@Transactional標註的任意方法:@annotation(org.springframework.transaction.annotation.Transactional)
    @within和@target針對類的註解,@annotation是針對方法的註解
  • 參數帶有@Transactional標註的方法:@args(org.springframework.transaction.annotation.Transactional)
  • 參數爲String類型(運行是決定)的方法: args(String)

 

JoinPoint

當使用@Around處理時,需要將第一個參數定義爲ProceedingJoinPoint類型,該類是JoinPoint的子類。

常用的方法:

  • Object[] getArgs:返回目標方法的參數
  • Signature getSignature:返回目標方法的簽名
  • Object getTarget:返回被織入增強處理的目標對象
  • Object getThis:返回AOP框架爲目標對象生成的代理對象

 

ProceedingJoinPoint獲取當前方法

Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();

// 上面這種方式獲取到的方法是接口的方法而不是具體的實現類的方法,因此是錯誤的。

Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = null;
if (!(signature instanceof MethodSignature)) {
    throw new IllegalArgumentException("該註解只能用於方法");
}
methodSignature = (MethodSignature) signature;
Object target = joinPoint.getTarget();
Method currentMethod = target.getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());

 

實戰演練

添加jar包依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 

切面配置

@Component
@Aspect
@Slf4j
public class JaemonAop {
    /**
     * 前置通知, 方法調用前被調用
     * */
    @Before("executeService()")
    public void doBeforeAdvice(JoinPoint joinPoint){
        log.info("doBeforeAdvice: {}." + joinPoint.getSignature().getName());
    }


    /**
     * 匹配 com.jaemon.controller包及其子包下的所有類的所有方法
     *
     *      execution()                    表達式的主體
     *      第一個“*”符號                    表示返回值的類型任意
     *      com.jaemon.controller          AOP所切的服務的包名,即,需要進行橫切的業務類
     *      包名後面的“..”                   表示當前包及子包
     *      第二個“*”                       表示類名,*即所有類
     *      .*(..)                         表示任何方法名,括號表示參數,兩個點表示任何參數類型
     * */
    @Pointcut("execution(* com.jaemon.controller..*.*(..))")
    public void executeService(){

    }


    /**
     * 後置最終通知, 目標方法只要執行完了就會執行後置通知方法
     * */
    @After("executeService()")
    public void doAfterAdvice(JoinPoint joinPoint){
        log.info("doAfterAdvice: {}.", joinPoint.getSignature().getName());

    }


    /**
     * 後置返回通知
     *  這裏需要注意的是:
     *      如果參數中的第一個參數爲JoinPoint, 則第二個參數爲返回值的信息
     *      如果參數中的第一個參數不爲JoinPoint, 則第一個參數爲returning中對應的參數
     * returning 限定了只有目標方法返回值與通知方法相應參數類型時才能執行後置返回通知, 否則不執行
     * 對於returning對應的通知方法參數爲Object類型將匹配任何目標返回值
     * */
    @AfterReturning(value = "execution(* com.jaemon.controller..*.*(..))", returning = "response")
    public void doAfterReturningAdvice(JoinPoint joinPoint, Object response){
        log.info("doAfterReturningAdvice: {} {}.", joinPoint.getSignature().getName(), JSON.toJSON(response));
    }


    /**
     * 後置異常通知
     *  第二個參數爲需要切點匹配的異常類型, 如(JoinPoint joinPoint, NullPointerExceptionrowable exception), 則只會匹配拋出空指針異常的方法
     *  對於throwing對應的通知方法參數爲Throwable類型將匹配任何異常
     * */
    @AfterThrowing(value = "executeService()", throwing = "exception")
    public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception) {
        log.info("doAfterThrowingAdvice: {}.", joinPoint.getSignature().getName());
        if (exception instanceof BusinessException) {
            log.info("exception type BusinessException");
        }
    }

    /**
     * 環繞通知
     *  環繞通知非常強大, 可以決定目標方法是否執行,什麼時候執行,執行時是否需要替換方法參數,執行完畢是否需要替換返回值
     *  環繞通知第一個參數必須是org.aspectj.lang.ProceedingJoinPoint類型
     * */
    @Around("execution(* com.jaemon.controller..*.*(..))")
    public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        log.info("AOP method name: {}, method modifiers: {}, method args: {}.",
                proceedingJoinPoint.getSignature().getName(), proceedingJoinPoint.getSignature().getModifiers(), proceedingJoinPoint.getArgs());
        try {
            return proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }
}

 

自定義註解切面

自定義註解

@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.METHOD
})
@Documented
public @interface WebLog {

    String type() default "";

    String sign() default "";

}

切面配置

@Aspect
@Component
@Slf4j
public class LogAspect {

    @Pointcut(value = "@annotation(com.jaemon.app.aop.WebLog)")
    private void pointCut() {

    }

    @Around(value = "pointCut() && @annotation(webLog)")
    public Object around(ProceedingJoinPoint proceedingJoinPoint, WebLog webLog) {
        log.info("type=[{}], sign=[{}].", webLog.type(), webLog.sign());
        try {
            return proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }

}

接口代碼

@RequestMapping("/menu")
@WebLog(sign = "queryMenu", type = "all")
public Response menu() {
	// ...
	return Response.success();
}

 

Reference

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章