Spring Boot2.x 中的切面編程,實例場景講解(日誌打印)

1.什麼是切面編程AOP

官方解釋:

AOP通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種技術。可以在代碼階段實現分離關注,實現松耦合,然後在運行時將相關動作特徵植入運行,從而改變程序的運行順序,達到攔截或者增加功能的目的。

好吧,我承認,讀起來很拗口,而且估計你讀完一遍再讀一遍,也不能理解是什麼意思。

下面我提出一種場景,看看你會怎麼實現,用此例子來理解切面編程。

有A,B, C三個方法,但是在調用每一個方法之前,要求打印一個日誌:某一個方法被開始調用了!在調用每個方法之後,也要求打印日誌:某個方法被調用完了!統計一下每個方法執行的耗時。

一般人會在每一個方法的開始和結尾部分都會添加一句日誌打印吧,這樣做如果方法多了,就會有很多重複的代碼,顯得很麻煩,這時候有人會想到,爲什麼不把打印日誌這個功能封裝一下,然後讓它能在指定的地方(比如執行方法前,或者執行方法後)自動的去調用呢?這樣做也是比較囉嗦,不能讓開發者專業的去做業務開發。

如果可以的話,業務功能代碼中就不會摻雜這一下其他的代碼,所以AOP就是做了這一類的工作,比如,日誌輸出,事務控制,異常的處理等。

下面介紹一下Springboot怎麼集成AOP

2.Spring Boot 集成AOP

2.1添加maven依賴

使用AOP,首先要引入對AOP的依賴,如下:

<!--Aop的依賴-->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-aop</artifactId>
 </dependency>

2.2 切面編程 (實現日誌打印實例)

這是幾個重要的註解:

1.@Pointcut:定義一個切面,即上面所描述的關注的某件事入口。
2.@Before:在做某件事之前做的事。
3.@After:在做某件事之後做的事。
4.@AfterReturning:在做某件事之後,對其返回值做增強處理。
5.@AfterThrowing:在做某件事拋出異常時,處理。
6.@Around:環繞建言(advice),在原方法執行前執行,在原方法執行後再執行(@Around可以實現其他兩種advice)

2.2.1 創建切面類LogAspectHandler.java

Spring Boot 中集成 AOP 非常簡單,假如我們要在項目中打印一些日誌,我們新建一個類 LogAspectHandler,用來定義切面和處理方法。只要在類上加個@Aspect註解即可。@Aspect 註解用來描述一個切面類,定義切面類的時候需要打上這個註解。@Component 註解是把此類交給 Spring 來管理。

@Aspect
@Component
public class LogAspectHandler {

}

2.2.2 @Pointcut:切點

@Pointcut 註解:用來定義一個切面(切入點),即定義攔截規則,確定有哪些方法會被切入。切入點決定了連接點關注的內容,使得我們可以控制通知什麼時候執行。

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

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

首先以 execution(* com.ieslab.powergrid.demosvr.controller..*.*(..))) 表達式爲例,語法如下:

  1. execution() 爲表達式主體
  2. 第一個 * 號的位置:表示返回值類型,* 表示所有類型
  3. 包名:表示需要攔截的包名,後面的兩個句點表示當前包和當前包的所有子包,com.itcodai.course09.controller 包、子包下所有類的方法
  4. 第二個 * 號的位置:表示類名,* 表示所有類
  5. *(..) :這個星號表示方法名,* 表示所有的方法,後面括弧裏面表示方法的參數,兩個句點表示任何參數

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

@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public void annotationCut() {}

然後使用該切面的話,就會切入註解是 @GetMapping 的方法。因爲在實際項目中,可能對於不同的註解有不同的邏輯處理,比如 @GetMapping@PostMapping@DeleteMapping 等。所以這種按照註解的切入方式在實際項目中也很常用。

在實際的應用中,我們有時也會自定義註解,並按照自定義註解進行定義切面,這樣就可以針對添加自定義註解的方法進行攔截。

2.2.3 @Before:前置建言

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

package com.ieslab.powergrid.demosvr.utils;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/** <p>Title: LogAspectHandler </p>
 * <p>Description: 日誌切面處理類 </p>
 *
 * @author bingge
 * @date 2020-2-20 下午7:15:30
 * @version V1.0
 */
@Aspect
@Component
@Slf4j
public class LogAspectHandler {
    /**
     * 定義一個切面,攔截com.ieslab.powergrid.demosvr.controller包和子包下的所有方法
     */
    @Pointcut("execution(* com.ieslab.powergrid.demosvr.controller..*.*(..))")
    public void pointCut() {}

    /**
     * 在上面定義的切面方法之前執行該方法
     * @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.2.4 @After:後置建言

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

增加代碼如下:

/**
     * 在上面定義的切面方法之後執行該方法
     * @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 如下:

package com.ieslab.powergrid.demosvr.controller;

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

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

    /**
     * 測試接口
     * @param name 名稱
     * @return
     */
    @GetMapping("/test/{name}")
    public String testAop(@PathVariable String name) {
        return "Hello " + name;
    }

}

啓動項目,在瀏覽器中輸入 http://localhost:8080/aop/test/binge,看一下控制檯的輸出信息:
在這裏插入圖片描述
如果所示,前兩步增加的日誌都已經打印出來。

2.2.5 @AfterReturning

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

例如:在LogAspectHandler.java類中繼續增加如下代碼:

    /**
     * 可以捕獲返回對象或者對返回對象進行增強
     * @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 + "增強版");
    }

運行程序,繼續在瀏覽器中輸入 http://localhost:8080/aop/test/binge,觀察控制檯日誌:
在這裏插入圖片描述

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

2.2.6 @AfterThrowing

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

在LogAspectHandler.java類中繼續增加如下代碼:

/**
 * 在上面定義的切面方法執行拋異常時,執行該方法
 * @param joinPoint 切入點
 * @param 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);
}

在TestController類中添加如下代碼,故意製造異常:

@GetMapping("/testException")
public String testException() {
    int num = 1/0;
    return "Hello world";
}

重新啓動程序,繼續在瀏覽器中輸入 http://localhost:8080/aop/test/binge,觀察控制檯日誌:
在這裏插入圖片描述

2.2.7 @Around:環繞

環繞建言(advice),在原方法執行前執行,在原方法執行後再執行(@Around可以實現其他兩種advice)。此處不再介紹,請自己試試。

3.有什麼應用場景

權限控制、日誌輸出,事務控制,異常的處理。
日誌場景已經介紹,後續會在其他文章介紹其他場景的使用。

4.總結

本節課針對Spring Boot中的切面 AOP 做了詳細的講解,主要介紹了 Spring BootAOP的引入,常用註解的使用,參數的使用,以及常用api的介紹。

AOP在實際項目中很有用,對切面方法執行前後都可以根據具體的業務,做相應的預處理或者增強處理,同時也可以用作異常捕獲處理,可以根據具體業務場景,合理去使用AOP

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