【SpringBoot註解-2】AOP相關注解詳解

前言

此文,記錄了以下內容:

  1. 示例:通過AOP實現接口(參數類型爲JSONObject)的參數校驗,以及多個切面類的執行順序問題
  2. @Pointcut@Around等註解的解讀
  3. @Around的使用示例:如何通過@Around修改目標方法參數

1 AOP簡介

AOP(Aspect Oriented Programming)是面向切面的編程,其編程思想是把散佈於不同業務但功能相同的代碼從業務邏輯中抽取出來,封裝成獨立的模塊,這些獨立的模塊被稱爲切面,切面的具體功能方法被稱爲關注點。在業務邏輯執行過程中,AOP會把分離出來的切面和關注點動態切入到業務流程中,這樣做的好處是提高了功能代碼的重用性和可維護性。

以現實中的例子爲例,一個公司會有很多相對獨立的部門,分別負責一些業務,比如技術部負責技術研發,市場運營部負責開拓市場,人力資源部負責公司人事,每個部門就可以視爲一個切面。如果項目業務擴展,增加新的切面,就類似公司開設新的部門。這樣,項目就形成了一種可配置、可插拔的程序結構。
下圖(來自網絡)就非常形象地展示了AOP的特徵:
在這裏插入圖片描述

2 SpringBoot中的AOP處理

2.1 AOP 環境

使用 AOP,首先需要引入 AOP 的依賴。

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

2.2 實現 AOP 切面

下面我們先用一個簡略但完整的權限校驗的示例展示aop的使用,該例的場景是:

  1. 自定義一個註解PermissionsAnnotation
  2. 創建一個切面類,切點設置爲攔截所有標註PermissionsAnnotation的方法,其邏輯爲截取到接口的參數,進行簡單的權限校驗
  3. PermissionsAnnotation標註在測試接口類的測試接口test

具體實現如下:

  1. 自定義一個註解,只要在類上加個 @Aspect 註解即可。@Aspect 註解用來描述一個切面類,定義切面類的時候需要打上這個註解。@Component 註解將該類交給 Spring 來管理。:
@Aspect
@Component
public class PermissionsAnnotation {

}
  1. 創建第一個AOP切面類,在這個類裏實現第一步權限校驗邏輯:
package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class PermissionFirstAdvice {

    @Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
    private void permissionCheck() {
    }


    @Around("permissionCheck()")
    public Object permissionCheckFirst(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("===================第一個切面===================:" + System.currentTimeMillis());

        //獲取請求參數,詳見接口類
        Object[] objects = joinPoint.getArgs();
        Long id = ((JSONObject) objects[0]).getLong("id");
        String name = ((JSONObject) objects[0]).getString("name");
        System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);
        System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);

        // id小於0則拋出非法id的異常
        if (id < 0) {
            return JSON.parseObject("{\"message\":\"illegal id\",\"code\":403}");
        }
        return joinPoint.proceed();
    }
}

  1. 創建第二個AOP切面類,在這個類裏實現第二步權限校驗。之所以特地寫兩個切面類,是爲了演示AOP切面執行順序的問題。注意類上的Order註解,註解後面的數字越小,將越先執行
package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Order(1)
public class PermissionSecondAdvice {

    @Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
    private void permissionCheck() {
    }

    @Around("permissionCheck()")
    public Object permissionCheckSecond(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("===================第二個切面===================:" + System.currentTimeMillis());

        //獲取請求參數,詳見接口類
        Object[] objects = joinPoint.getArgs();
        Long id = ((JSONObject) objects[0]).getLong("id");
        String name = ((JSONObject) objects[0]).getString("name");
        System.out.println("id->>>>>>>>>>>>>>>>>>>>>>" + id);
        System.out.println("name->>>>>>>>>>>>>>>>>>>>>>" + name);

        // name不是管理員則拋出異常
        if (!name.equals("admin")) {
            return JSON.parseObject("{\"message\":\"not admin\",\"code\":403}");
        }
        return joinPoint.proceed();
    }
}
  1. 創建接口類,並在目標方法上標註自定義註解 PermissionsAnnotation
package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/permission")
public class TestController {
    @RequestMapping(value = "/check", method = RequestMethod.POST)
    @PermissionsAnnotation()
    public JSONObject getGroupList(@RequestBody JSONObject request) {
        return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200}");
    }
}

使用JMeter調用接口進行測試:
在這裏插入圖片描述
在這裏插入圖片描述

2.3 常用註解

上面的案例中,用到了諸多註解,下面針對這些註解進行詳解。

2.3.1 @Pointcut

@Pointcut 註解,用來定義一個切面,即上文中所關注的某件事情的入口,切入點定義了事件觸發時機。

@Aspect
@Component
public class LogAspectHandler {

    /**
     * 定義一個切面,攔截 com.itcodai.course09.controller 包和子包下的所有方法
     */
    @Pointcut("execution(* com.mutest.controller..*.*(..))")
    public void pointCut() {}
}

@Pointcut 註解指定一個切面,定義需要攔截的東西,這裏介紹兩個常用的表達式:一個是使用 execution(),另一個是使用 annotation()。

execution表達式:

execution(* * com.mutest.controller..*.*(..))) 表達式爲例:

  • 第一個 * 號的位置:表示返回值類型,* 表示所有類型。
  • 包名:表示需要攔截的包名,後面的兩個句點表示當前包和當前包的所有子包,在本例中指 com.mutest.controller包、子包下所有類的方法。
  • 第二個 * 號的位置:表示類名,* 表示所有類。
  • *(..):這個星號表示方法名,* 表示所有的方法,後面括弧裏面表示方法的參數,兩個句點表示任何參數。

annotation() 表達式:

annotation() 方式是針對某個註解來定義切面,比如我們對具有 @PostMapping 註解的方法做切面,可以如下定義切面:

@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
public void annotationPointcut() {}

然後使用該切面的話,就會切入註解是 @PostMapping 的所有方法。這種方式很適合處理 @GetMapping、@PostMapping、@DeleteMapping不同註解有各種特定處理邏輯的場景。

還有就是如上面案例所示,針對自定義註解來定義切面。

@Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
private void permissionCheck() {}

2.3.2 @Around

@Around註解用於修飾Around增強處理,Around增強處理非常強大,表現在:

  1. @Around可以自由選擇增強動作與目標方法的執行順序,也就是說可以在增強動作前後,甚至過程中執行目標方法。這個特性的實現在於,調用ProceedingJoinPoint參數的procedd()方法纔會執行目標方法。
  2. @Around可以改變執行目標方法的參數值,也可以改變執行目標方法之後的返回值。

Around增強處理有以下特點:

  1. 當定義一個Around增強處理方法時,該方法的第一個形參必須是 ProceedingJoinPoint 類型(至少一個形參)。在增強處理方法體內,調用ProceedingJoinPoint的proceed方法纔會執行目標方法:這就是@Around增強處理可以完全控制目標方法執行時機、如何執行的關鍵;如果程序沒有調用ProceedingJoinPointproceed方法,則目標方法不會執行。
  2. 調用ProceedingJoinPoint的proceed方法時,還可以傳入一個Object[ ]對象,該數組中的值將被傳入目標方法作爲實參——這就是Around增強處理方法可以改變目標方法參數值的關鍵。這就是如果傳入的Object[ ]數組長度與目標方法所需要的參數個數不相等,或者Object[ ]數組元素與目標方法所需參數的類型不匹配,程序就會出現異常。

@Around功能雖然強大,但通常需要在線程安全的環境下使用。因此,如果使用普通的Before、AfterReturning就能解決的問題,就沒有必要使用Around了。如果需要目標方法執行之前和之後共享某種狀態數據,則應該考慮使用Around。尤其是需要使用增強處理阻止目標的執行,或需要改變目標方法的返回值時,則只能使用Around增強處理了。

下面,在前面例子上做一些改造,來觀察@Around的特點。

自定義註解類不變。首先,定義接口類:

package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/permission")
public class TestController {
    @RequestMapping(value = "/check", method = RequestMethod.POST)
    @PermissionsAnnotation()
    public JSONObject getGroupList(@RequestBody JSONObject request) {
        return JSON.parseObject("{\"message\":\"SUCCESS\",\"code\":200,\"data\":" + request + "}");
    }
}

唯一切面類(前面案例有兩個切面類,這裏只需保留一個即可):

package com.example.demo;

import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Aspect
@Component
@Order(1)
public class PermissionAdvice {

    @Pointcut("@annotation(com.example.demo.PermissionsAnnotation)")
    private void permissionCheck() {
    }


    @Around("permissionCheck()")
    public Object permissionCheck(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("===================開始增強處理===================");

        //獲取請求參數,詳見接口類
        Object[] objects = joinPoint.getArgs();
        Long id = ((JSONObject) objects[0]).getLong("id");
        String name = ((JSONObject) objects[0]).getString("name");
        System.out.println("id1->>>>>>>>>>>>>>>>>>>>>>" + id);
        System.out.println("name1->>>>>>>>>>>>>>>>>>>>>>" + name);

		// 修改入參
        JSONObject object = new JSONObject();
        object.put("id", 8);
        object.put("name", "lisi");
        objects[0] = object;
		
		// 將修改後的參數傳入
        return joinPoint.proceed(objects);
    }
}

同樣使用JMeter調用接口,傳入參數:{"id":-5,"name":"admin"}
在這裏插入圖片描述

2.3.3 @Before

@Before 註解指定的方法在切面切入目標方法之前執行,可以做一些 Log 處理,也可以做一些信息的統計,比如獲取用戶的請求 URL 以及用戶的 IP 地址等等,這個在做個人站點的時候都能用得到,都是常用的方法。例如下面代碼:

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * 在上面定義的切面方法之前執行該方法
     * @param joinPoint jointPoint
     */
    @Before("pointCut()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("====doBefore方法進入了====");

        // 獲取簽名
        Signature signature = joinPoint.getSignature();
        // 獲取切入的包名
        String declaringTypeName = signature.getDeclaringTypeName();
        // 獲取即將執行的方法名
        String funcName = signature.getName();
        log.info("即將執行方法爲: {},屬於{}包", funcName, declaringTypeName);

        // 也可以用來記錄一些信息,比如獲取請求的 URL 和 IP
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 獲取請求 URL
        String url = request.getRequestURL().toString();
        // 獲取請求 IP
        String ip = request.getRemoteAddr();
        log.info("用戶請求的url爲:{},ip地址爲:{}", url, ip);
    }
}

JointPoint 對象很有用,可以用它來獲取一個簽名,利用簽名可以獲取請求的包名、方法名,包括參數(通過 joinPoint.getArgs() 獲取)等。

2.3.4 @After

@After 註解和 @Before 註解相對應,指定的方法在切面切入目標方法之後執行,也可以做一些完成某方法之後的 Log 處理。

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * 定義一個切面,攔截 com.mutest.controller 包下的所有方法
     */
    @Pointcut("execution(* com.mutest.controller..*.*(..))")
    public void pointCut() {}

    /**
     * 在上面定義的切面方法之後執行該方法
     * @param joinPoint jointPoint
     */
    @After("pointCut()")
    public void doAfter(JoinPoint joinPoint) {

        log.info("==== doAfter 方法進入了====");
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        log.info("方法{}已經執行完", method);
    }
}

到這裏,我們來寫個 Controller 測試一下執行結果,新建一個 AopController 如下:

@RestController
@RequestMapping("/aop")
public class AopController {

    @GetMapping("/{name}")
    public String testAop(@PathVariable String name) {
        return "Hello " + name;
    }
}

啓動項目,在瀏覽器中輸入:localhost:8080/aop/csdn,觀察一下控制檯的輸出信息:

====doBefore 方法進入了====  
即將執行方法爲: testAop,屬於com.itcodai.mutest.AopController包  
用戶請求的 url 爲:http://localhost:8080/aop/name,ip地址爲:0:0:0:0:0:0:0:1  
==== doAfter 方法進入了====  
方法 testAop 已經執行完

從打印出來的 Log 中可以看出程序執行的邏輯與順序,可以很直觀的掌握 @Before 和 @After 兩個註解的實際作用。

2.3.5 @AfterReturning

@AfterReturning 註解和 @After 有些類似,區別在於 @AfterReturning 註解可以用來捕獲切入方法執行完之後的返回值,對返回值進行業務邏輯上的增強處理,例如:

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * 在上面定義的切面方法返回後執行該方法,可以捕獲返回對象或者對返回對象進行增強
     * @param joinPoint joinPoint
     * @param result result
     */
    @AfterReturning(pointcut = "pointCut()", returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Object result) {

        Signature signature = joinPoint.getSignature();
        String classMethod = signature.getName();
        log.info("方法{}執行完畢,返回參數爲:{}", classMethod, result);
        // 實際項目中可以根據業務做具體的返回值增強
        log.info("對返回參數進行業務上的增強:{}", result + "增強版");
    }
}

需要注意的是,在 @AfterReturning 註解 中,屬性 returning 的值必須要和參數保持一致,否則會檢測不到。該方法中的第二個入參就是被切方法的返回值,在 doAfterReturning 方法中可以對返回值進行增強,可以根據業務需要做相應的封裝。我們重啓一下服務,再測試一下:

方法 testAop 執行完畢,返回參數爲:Hello CSDN  
對返回參數進行業務上的增強:Hello CSDN 增強版

2.3.6 @AfterThrowing

當被切方法執行過程中拋出異常時,會進入 @AfterThrowing 註解的方法中執行,在該方法中可以做一些異常的處理邏輯。要注意的是 throwing 屬性的值必須要和參數一致,否則會報錯。該方法中的第二個入參即爲拋出的異常。

@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * 在上面定義的切面方法執行拋異常時,執行該方法
     * @param joinPoint jointPoint
     * @param ex ex
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Throwable ex) {
        Signature signature = joinPoint.getSignature();
        String method = signature.getName();
        // 處理異常的邏輯
        log.info("執行方法{}出錯,異常爲:{}", method, ex);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章