Spring Boot AOP 掃盲,實現介面訪問的統一日誌記錄

沉默王二發表於2022-02-22

AOP 是 Spring 體系中非常重要的兩個概念之一(另外一個是 IoC),今天這篇文章就來帶大家通過實戰的方式,在程式設計貓 SpringBoot 專案中使用 AOP 技術為 controller 層新增一個切面來實現介面訪問的統一日誌記錄。

一、關於 AOP

AOP,也就是 Aspect-oriented Programming,譯為面向切面程式設計,是電腦科學中的一個設計思想,旨在通過切面技術為業務主體增加額外的通知(Advice),從而對宣告為“切點”(Pointcut)的程式碼塊進行統一管理和裝飾。

這種思想非常適用於,將那些與核心業務不那麼密切關聯的功能新增到程式中,就好比我們今天的主題——日誌功能,就是一個典型的案例。

AOP 是對物件導向程式設計(Object-oriented Programming,俗稱 OOP)的一種補充,OOP 的核心單元是類(class),而 AOP 的核心單元是切面(Aspect)。利用 AOP 可以對業務邏輯的各個部分進行隔離,從而降低耦合度,提高程式的可重用性,同時也提高了開發效率。

我們可以簡單的把 AOP 理解為貫穿於方法之中,在方法執行前、執行時、執行後、返回值後、異常後要執行的操作。

二、AOP 的相關術語

來看下面這幅圖,這是一個 AOP 的模型圖,就是在某些方法執行前後執行一些通用的操作,並且這些操作不會影響程式本身的執行。


我們瞭解下 AOP 涉及到的 5 個關鍵術語:

1)橫切關注點,從每個方法中抽取出來的同一類非核心業務

2)切面(Aspect),對橫切關注點進行封裝的類,每個關注點體現為一個通知方法;通常使用 @Aspect 註解來定義切面。

3)通知(Advice),切面必須要完成的各個具體工作,比如我們的日誌切面需要記錄介面呼叫前後的時長,就需要在呼叫介面前後記錄時間,再取差值。通知的方式有五種:

  • @Before:通知方法會在目標方法呼叫之前執行
  • @After:通知方法會在目標方法呼叫後執行
  • @AfterReturning:通知方法會在目標方法返回後執行
  • @AfterThrowing:通知方法會在目標方法丟擲異常後執行
  • @Around:把整個目標方法包裹起來,在被呼叫前和呼叫之後分別執行通知方法

4)連線點(JoinPoint),通知應用的時機,比如介面方法被呼叫時就是日誌切面的連線點。

5)切點(Pointcut),通知功能被應用的範圍,比如本篇日誌切面的應用範圍是所有 controller 的介面。通常使用 @Pointcut 註解來定義切點表示式。

切入點表示式的語法格式規範如下所示:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?
				name-pattern(param-pattern)
                throws-pattern?)
  • modifiers-pattern? 為訪問許可權修飾符
  • ret-type-pattern 為返回型別,通常用 * 來表示任意返回型別
  • declaring-type-pattern? 為包名
  • name-pattern 為方法名,可以使用 * 來表示所有,或者 set* 來表示所有以 set 開頭的類名
  • param-pattern) 為引數型別,多個引數可以用 , 隔開,各個參與也可以使用 * 來表示所有型別的引數,還可以使用 (..) 表示零個或者任意引數
  • throws-pattern? 為異常型別
  • ? 表示前面的為可選項

舉個例子:

@Pointcut("execution(public * com.codingmore.controller.*.*(..))")

表示 com.codingmore.controller 包下的所有 public 方法都要應用切面的通知。

三、實操 AOP 記錄介面訪問日誌

第一步,在 Spring Boot 專案的 pom.xml 檔案中新增 spring-boot-starter-aop 依賴。

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

第二步,新增日誌資訊封裝類 WebLog,用於記錄什麼樣的操作、操作的人是誰、開始時間、花費的時間、操作的路徑、操作的方法名、操作主機的 IP、請求引數、返回結果等。

/**
 * Controller層的日誌封裝類
 * Created by macro on 2018/4/26.
 */
public class WebLog {
    private String description;
    private String username;
    private Long startTime;
    private Integer spendTime;
    private String basePath;
    private String uri;
    private String url;
    private String method;
    private String ip;
    private Object parameter;
    private Object result;
    //省略了getter,setter方法
}

第三步,新增統一日誌處理切面 WebLogAspect。

/**
 * 統一日誌處理切面
 * Created by 石磊
 */
@Aspect
@Component
@Order(1)
public class WebLogAspect {
    private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);

    @Pointcut("execution(public * com.codingmore.controller.*.*(..))")
    public void webLog() {
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
    }

    @AfterReturning(value = "webLog()", returning = "ret")
    public void doAfterReturning(Object ret) throws Throwable {
    }

    @Around("webLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        //獲取當前請求物件
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //記錄請求資訊(通過Logstash傳入Elasticsearch)
        WebLog webLog = new WebLog();
        Object result = joinPoint.proceed();
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method.isAnnotationPresent(ApiOperation.class)) {
            ApiOperation log = method.getAnnotation(ApiOperation.class);
            webLog.setDescription(log.value());
        }
        long endTime = System.currentTimeMillis();
        String urlStr = request.getRequestURL().toString();
        webLog.setBasePath(StrUtil.removeSuffix(urlStr, URLUtil.url(urlStr).getPath()));
        webLog.setIp(request.getRemoteUser());
        Map<String,Object> logMap = new HashMap<>();
        logMap.put("spendTime",webLog.getSpendTime());
        logMap.put("description",webLog.getDescription());
        LOGGER.info("{}", JSONUtil.parse(webLog));
        return result;
    }
}

第四步,執行專案,並對 controller 下的某個控制器進行測試。

Swagger knife4j 訪問地址:http://localhost:9022/doc.html

執行登入使用者查詢操作:

可以在控制檯可以看到以下日誌資訊:


原始碼地址:

https://github.com/itwanger/coding-more

參考連結:

作者 cxuan:https://www.cnblogs.com/cxuanBlog/p/13060510.html

灰小猿:https://bbs.huaweicloud.com/blogs/289045

山高我為峰:https://www.cnblogs.com/liaojie970/p/7883687.html

macrozheng:https://github.com/macrozheng/mall


本篇已收錄至 GitHub 上星標 1.6k+ star 的開源專欄《Java 程式設計師進階之路》,據說每一個優秀的 Java 程式設計師都喜歡她,風趣幽默、通俗易懂。內容包括 Java 基礎、Java 併發程式設計、Java 虛擬機器、Java 企業級開發、Java 面試等核心知識點。學 Java,就認準 Java 程式設計師進階之路?。

https://github.com/itwanger/toBeBetterJavaer

star 了這個倉庫就等於你擁有了成為了一名優秀 Java 工程師的潛力。也可以戳下面的連結跳轉到《Java 程式設計師進階之路》的官網網址,開始愉快的學習之旅吧。

https://tobebetterjavaer.com/

沒有什麼使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不繫之舟

相關文章