Spring AOP @Before @Around @After等執行順序

1.AOP基本概念

切面(Aspect):通知(advice)和切入點(pointcut)共同組成了切面(aspect)

切入點(Pointcut):匹配join point的謂詞,切面切向哪裏,某個類或者某一層包路徑

連接點(Joinpoint):aop攔截的類或者方法,例如方法被調用時、異常被拋出時。(在Spring中,所有的方法都可以認爲是joinpoint,但是我們不希望所有的方法都添加Advice,而pointcut的作用就是提供一組規則來匹配joinpoint,給滿足規則的joinpoint添加Advice。)

通知(Advice):切面何時使用,即注有@Around、@Before、@After等註解的方法

目標對象(Target Object):被通知的對象

AOP代理(AOP Proxy): AOP的代理有兩種,一種是JDK動態代理,一種是CGLIB代理。默認情況下,TargetObject實現了接口時,則採用JDK動態代理;反之,採用CGLIB代理

Joinpoint和Pointcut區別:在Spring AOP中,所有的方法執行都是joinpoint,而pointcut是一個描述信息,它修飾的是joinpoint,通過pointcut可以確定哪些jointpoint可以被Advice。

2.通知(Advice)類型說明

@Around:環繞通知,包圍一個連接點的通知,可以在覈心方法前後完成自定義的行爲。這是最常用最重要的。這個註解需要傳入參數ProceedingJoinPoint pjp,決定是否進入核心方法----調用pjp.proceed();如果不調的話將不進入核心方法!

@Before:前通知,核心代碼執行前通知

@After:後通知,連接點執行退出時通知(不論正常返回還是異常退出)

@AfterReturning:返回後通知,正常返回後通知

@AfterThrowing:拋出異常後通知,拋出異常時通知

注意:除了@Around傳的參數是ProceedingJoinPoint pjp外,其它都是傳的JoinPoint jp,也就是說能控制是否進入核心代碼的只有Around,因爲aop走到核心代碼就是通過調用ProceedingJoinPoint的proceed()方法,而JoinPoint沒有這個方法。

3.Advice順序:

情況一:一個方法只被一個Aspect類攔截

添加PointCut類  添加Aspect類   這兩個可以放在一起

package com.bob.hello.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * Description
 *
 * @author Bob
 * @date 2020/6/2
 **/
@Component
@Aspect
public class DemoAspect {

    /**
     * @description 攔截com.bob.hello包下所有方法
     * @author Bob
     * @date 2020/6/2
     */
    @Pointcut(value = "within(com.bob.hello.controller.*)")
    public void demoPointCut() {

    }

    @Before(value = "demoPointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("[DemoAspect] before advice");
    }

    @Around(value = "demoPointCut()")
    public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("[DemoAspect] around advice 1");
        proceedingJoinPoint.proceed();
        System.out.println("[DemoAspect] around advice 2");
    }

    @After(value = "demoPointCut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("[DemoAspect] after advice");
    }

    @AfterThrowing(value = "demoPointCut()")
    public void afterThrowing(JoinPoint joinPoint) {
        System.out.println("[DemoAspect] afterThrowing advice");
    }

    @AfterReturning(value = "demoPointCut()")
    public void afterReturning(JoinPoint joinPoint) {
        System.out.println("[DemoAspect] afterReturning advice");
    }
}

添加測試用的Controller

package com.bob.hello.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Description
 *
 * @author Bob
 * @date 2020/5/26
 **/
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        System.out.println("hello");
        return "hello";
    }
}

測試 正常情況

瀏覽器直接輸入:http://localhost:8080/hello

輸出的結果:

[DemoAspect] around advice 1
[DemoAspect] before advice
hello
[DemoAspect] around advice 2
[DemoAspect] after advice
[DemoAspect] afterReturning advice

注意:如果用到了Spring Security,會先走登錄攔截

結論

在一個方法只被一個aspect類攔截時,aspect類內部的 advice 將按照以下的順序進行執行:

one-ok

情況二:同一個方法被多個Aspect類攔截

aspect1和aspect2的執行順序是未知的,除非設置他們的Order順序,具體參見:Spring AOP @Before @Around @After 等 advice 的執行順序

4.定義Aspect(切面)

當使用@Aspect標註一個bean後,Spring框架會自動收集這些bean,並添加到Spring AOP中,例如:

@Component
@Aspect
public class DemoAspect {

}

注意:僅僅使用@Aspect 註解並不能將一個Java對象轉換爲bean,因此還需要類似@Component 之類的註解。

注意:如果一個類被@Aspect 標註,則這個類就不能是其它aspect的**advised object** 了,因爲使用@Aspect後,這個類就會別排除在auto-proxying機制之外。

5.聲明Pointcut(切入點)

在@AspectJ 風格的AOP中,這樣來描述pointcut,例如:

@Pointcut(value = "within(com.bob.hello.controller.*)")
public void demoPointCut() {

}

value裏面的值叫pointcut(切點)表達式,它由標識符操作參數組成,within就是標識符,()裏的就是操作參數。

常見的切點表達式

匹配方法簽名

// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))

// 匹配當前包中的指定類的所有方法
execution(* UserService.*(..))

// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))

// 匹配指定包中的所有 public 方法, 並且返回值是 int 類型的方法
execution(public int com.xys.service.*(..))

// 匹配指定包中的所有 public 方法, 並且第一個參數是 String, 返回值是 int 類型的方法
execution(public int com.xys.service.*(String name, ..))

匹配類型簽名

// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)

// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)

// 匹配當前包中的指定類中的方法
within(UserService)

// 匹配一個接口的所有實現類中的實現的方法
within(UserDao+)

匹配Bean名字

// 匹配以指定名字結尾的 Bean 中的所有方法
bean(*Service)

切點表達式組合

// 匹配以 Service 或 ServiceImpl 結尾的 bean
bean(*Service || *ServiceImpl)

// 匹配名字以 Service 開頭, 並且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)

參考:徹底征服 Spring AOP 之 理論篇 

6.聲明Advice(通知)

示例中的@Before @Around等

7.Spring AOP實戰

HTTP接口鑑權

核心代碼:

@Component
@Aspect
public class HttpAopAdviseDefine {

    // 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.
    @Pointcut("@annotation(com.xys.demo1.AuthChecker)")
    public void pointcut() {
    }

    // 定義 advise
    @Around("pointcut()")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();

        // 檢查用戶所傳遞的 token 是否合法
        String token = getUserToken(request);
        if (!token.equalsIgnoreCase("123456")) {
            return "錯誤, 權限不合法!";
        }

        return joinPoint.proceed();
    }

    private String getUserToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if (cookies == null) {
            return "";
        }
        for (Cookie cookie : cookies) {
            if (cookie.getName().equalsIgnoreCase("user_token")) {
                return cookie.getValue();
            }
        }
        return "";
    }
}

方法調用日誌

核心代碼:

@Component
@Aspect
public class LogAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());

    // 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.
    @Pointcut("within(NeedLogService)")
    public void pointcut() {
    }

    // 定義 advise
    @Before("pointcut()")
    public void logMethodInvokeParam(JoinPoint joinPoint) {
        logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
    }

    @AfterReturning(pointcut = "pointcut()", returning = "retVal")
    public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) {
        logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
    }

    @AfterThrowing(pointcut = "pointcut()", throwing = "exception")
    public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) {
        logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage());
    }
}

方法耗時統計

核心代碼:

@Component
@Aspect
public class ExpiredAopAdviseDefine {
    private Logger logger = LoggerFactory.getLogger(getClass());

    // 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.
    @Pointcut("within(SomeService)")
    public void pointcut() {
    }

    // 定義 advise
    // 定義 advise
    @Around("pointcut()")
    public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // 開始
        Object retVal = pjp.proceed();
        stopWatch.stop();
        // 結束

        // 上報到公司監控平臺
        reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());

        return retVal;
    }


    public void reportToMonitorSystem(String methodName, long expiredTime) {
        logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);
        //
    }
}

來自:徹底征服 Spring AOP 之 實戰篇

參考:

Spring AOP @Before @Around @After 等 advice 的執行順序

spring aop的@Before,@Around,@After,@AfterReturn,@AfterThrowing的理解

徹底征服 Spring AOP 之 理論篇 

徹底征服 Spring AOP 之 實戰篇

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