Spring Boot實戰系列(3)AOP面向切面程式設計

五月君發表於2018-11-06

AOP是一種與語言無關的程式思想、程式設計正規化。專案業務邏輯中,將通用的模組以水平切割的方式進行分離統一處理,常用於日誌、許可權控制、異常處理等業務中。

快速導航

程式設計正規化主要以下幾大類

  • AOP(Aspect Oriented Programming)面向切面程式設計
  • OOP(Object Oriented Programming)物件導向程式設計
  • POP(procedure oriented programming)程式導向程式設計
  • FP(Functional Programming)面向函式程式設計

引入aop依賴

以下示例是基於Spring Boot實戰系列(2)資料儲存之Jpa操作MySQL chapter2-1可在Github獲取原始碼

專案根目錄 pom.xml 新增依賴 spring-boot-starter-aop

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
複製程式碼

aop註解

  • @Aspect: 切面,由通知和切入點共同組成,這個註解標註在類上表示為一個切面。
  • @Joinpoint: 連線點,被AOP攔截的類或者方法,在前置通知中有介紹使用@Joinpoint獲取類名、方法、請求引數。
  • Advice: 通知的幾種型別
    • @Before: 前置通知,在某切入點@Pointcut之前的通知
    • @After: 後置通知,在某切入點@Pointcut之後的通知無論成功或者異常。
    • @AfterReturning: 返回後通知,方法執行return之後,可以對返回的資料做加工處理。
    • @Around: 環繞通知,在方法的呼叫前、後執行。
    • @AfterThrowing: 丟擲異常通知,程式出錯跑出異常會執行該通知方法。
  • @Pointcut: 切入點,從哪裡開始。例如從某個包開始或者某個包下的某個類等。

實現日誌分割功能

目錄 aspect下 新建 httpAspect.java類,在收到請求之後先記錄請求的相關引數日誌資訊,請求成功完成之後列印響應資訊,請求處理報錯列印報錯日誌資訊。

httpAspect.java

package com.angelo.aspect;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@Aspect
@Component
public class HttpAspect {
    // 列印日誌模組
    private final static Logger logger = LoggerFactory.getLogger(HttpAspect.class);

    // 下面會一一介紹... 
複製程式碼

新增切入點

定義切入的入口在哪裡,封裝一個公共的方法實現複用

httpAspect.java

    /**
     *  定義一個公共的方法,實現服用
     *  攔截UserController下面的所有方法
     *  攔截UserController下面的userList方法裡的任何引數(..表示攔截任何引數)寫法:@Before("execution(public * com.angelo.controller.UserController.userList(..))")
     */
    @Pointcut("execution(public * com.angelo.controller.UserController.*(..))")
    public void log() {
    }
複製程式碼

前置通知

攔截方法之前的一段業務邏輯,獲取請求的一些資訊,其中用到了Gson處理物件轉json輸出

httpAspect.java

@Before("log()")
public void doBefore(JoinPoint joinPoint) {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();

    Map params = new HashMap();
    params.put("url", request.getRequestURL()); // 獲取請求的url
    params.put("method", request.getMethod()); // 獲取請求的方式
    params.put("ip", request.getRemoteAddr()); // 獲取請求的ip地址
    params.put("className", joinPoint.getSignature().getDeclaringTypeName()); // 獲取類名
    params.put("classMethod", joinPoint.getSignature().getName()); // 獲取類方法
    params.put("args", joinPoint.getArgs()); // 請求引數

    // 輸出格式化後的json字串
    Gson gson = new GsonBuilder().setPrettyPrinting().create();

    logger.info("REQUEST: {}", gson.toJson(params));
}
複製程式碼

後置通知

攔截方法之後的一段業務邏輯

httpAspect.java

@After("log()")
public void doAfter() {
    logger.info("doAfter");
}
複製程式碼

環繞通知

環繞通知是在方法的前後的一段邏輯操作,可以修改目標方法的返回值,第一個引數是org.aspectj.lang.ProceedingJoinPoint型別,注意這裡要呼叫執行目標方法proceed()獲取值返回,不然會造成空指標異常。在環繞通知裡面也可以捕獲錯誤返回。

httpAspect.java

@Around("log()")
public Object doAround(ProceedingJoinPoint point) {
    try {
        Object o =  point.proceed();
        System.out.println("方法環繞proceed,結果是 :" + o);
        logger.info("doAround1");

        return o;
    } catch (Throwable e) {
        // e.printStackTrace();
        logger.info("doAround2");

        return null;
    }
}
複製程式碼

返回後通知

在切入點完成之後的返回通知,此時就不會丟擲異常通知,除非返回後通知的業務邏輯報錯。

httpAspect.java

    /**
     * 獲取響應返回值
     * @param object
     */
    @AfterReturning(returning = "object", pointcut = "log()")
    public void doAfterReturning(Object object) {
        // logger.info("RESPONSE: {}", object); 會列印出一個物件,想列印出具體內容需要在定義模型處加上toString()

        logger.info("RESPONSE: {}", object.toString());
    }
複製程式碼

異常通知

丟擲異常後的通知,此時返回後通知@AfterReturning就不會執行。

httpAspect.java

@AfterThrowing(pointcut = "log()")
public void doAfterThrowing() {
    logger.error("doAfterThrowing: {}", " 異常情況!");
}
複製程式碼

一段段虛擬碼讀懂執行順序

try {
    // @Before 執行前通知

    // 執行目標方法

    // @Around 執行環繞通知 成功走finall,失敗走catch
} finally {
    // @After 執行後置通知

    // @AfterReturning 執行返回後通知
} catch(e) {
    // @AfterThrowing 丟擲異常通知
}
複製程式碼

測試正常異常兩種情況

測試之前先對controller/UserController.java檔案的userList方法增加了exception引數

    /**
     * 查詢使用者列表
     * @return
     */
    @RequestMapping(value = "/user/list/{exception}")
    public List<User> userList(@PathVariable("exception") Boolean exception) {
        if (exception) {
            throw new Error("測試丟擲異常!");
        }

        return userRepository.findAll();
    }
複製程式碼
  • 測試正常情況

curl 127.0.0.1:8080/user/list/false

正常情況返回值如下所示:

Spring Boot實戰系列(3)AOP面向切面程式設計

  • 測試異常情況

curl 127.0.0.1:8080/user/list/true

異常情況返回值如下所示:

圖片描述

通過以上兩種情況測試可以看到環繞通知在正常、異常兩種情況都可以執行到。

Github檢視本文完整示例 chapter3-1

作者:五月君
連結:www.imooc.com/article/259…
來源:慕課網
Github: Spring Boot實戰系列

相關文章