Spring Boot使用AOP打印接口請求日誌
Spring的兩大特性 IOC(自動注入)和AOP(面向切面)本文講的是使用AOP面向切面的特行,在用戶訪問系統時候,根據其訪問的控制層方法 打印對應的請求日誌,例如:客戶端瀏覽器型號,電腦系統型號, 方法執行時間,請求參數等等。。。。。。。
1.理解AOP
(1)AOP
就是在某一個類或方法執行前後打個標記,聲明在執行到這裏之前要先執行什麼,執行完這裏之後要接着執行什麼。
這就對應着AOP的一些專業術語以及使用場景:
Aspect
(切面): 聲明類似於Java中的類聲明,在Aspect中會包含一些Pointcut及相應的Advice。
Joint point
(連接點): 表示在程序中明確定義的點。包括方法的調用、對類成員的訪問等。
Pointcut
(切入點): 表示一個組Joint point,如方法名、參數類型、返回類型等等。
Advice
(通知): Advice定義了在Pointcut裏面定義的程序點具體要做的操作,它通過(before、around、After
: 在執行方法後調用Advice,after、return是方法正常返回後調用,after\throw是方法拋出異常後調用。
Before
: 在執行方法前調用Advice,比如請求接口之前的登錄驗證。
Around
: 在執行方法前後調用Advice,這是最常用的方法。
Finally
: 方法調用後執行Advice,無論是否拋出異常還是正常返回。
(2)AOP
再白話理解就是在項目原基礎功能之上,使用AOP添加新功能 ,不會更改原來代碼 例如充值 之前代碼是沒有充值成功發送短信這個功能的,現在有了這個發送短信的需求,在不改動代碼,或者所有的充值接口統一處理,那麼我們就可以在所有類似充值的控制訪問層使用Aop,充值成功後發送一條短信。。。。。。
2.本文使用
本文使用的是註解式AOP
所需依賴
<!-- web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok代碼簡化插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Aop組件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 分析客戶端信息的工具類-->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.20</version>
</dependency>
基礎使用演示
package com.leilei.web.controller.common;
import eu.bitwalker.useragentutils.UserAgent;
import java.time.LocalDateTime;
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* @author leilei
* @version 1.0
* @date 2020/01/20 19:21
* @desc: 日誌切面 打印請求日誌
*/
@Slf4j //lombok中日誌註解
@Aspect //表明是一個切面類
@Component //交給Spring管理
public class LogAop {
/** 進入方法時間戳 */
private Long startTime;
/** 方法結束時間戳(計時) */
private Long endTime;
public LogAop() {}
/** 自定義切點 */
private final String POINTCUT = "execution(* com.leilei.web.controller..*(..))";
/**
* 前置通知,方法之前執行
* @param joinPoint
*/
@Before(POINTCUT)
public void doBefore(JoinPoint joinPoint) {
// 獲取當前的HttpServletRequest對象
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 獲取請求頭中的User-Agent
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
// 打印請求的內容
startTime = System.currentTimeMillis();
log.info("請求開始時間:{}", LocalDateTime.now());
log.info("請求Url : {}", request.getRequestURL().toString());
log.info("請求方式 : {}", request.getMethod());
log.info("請求ip : {}", request.getRemoteAddr());
log.info("請求內容類型 : {}", request.getContentType());
log.info("請求參數 : {}", Arrays.toString(joinPoint.getArgs()));
// 系統信息
log.info("瀏覽器 : {}", userAgent.getBrowser().toString());
log.info("瀏覽器版本 : {}", userAgent.getBrowserVersion());
log.info("操作系統: {}", userAgent.getOperatingSystem().toString());
}
@After(POINTCUT)
public void doAfter(JoinPoint joinPoint) {
// System.out.println("doAfter");
}
/**
* 返回通知 正常結束時進入此方法
* @param ret
*/
@AfterReturning(returning = "ret", pointcut = POINTCUT)
public void doAfterReturning(Object ret) {
endTime = System.currentTimeMillis();
log.info("請求結束時間 : {}", LocalDateTime.now());
log.info("請求耗時 : {}", (endTime - startTime));
// 處理完請求,返回內容
log.info("請求返回 : {}", ret);
}
/**
* 異常通知: 1. 在目標方法非正常結束,發生異常或者拋出異常時執行
*
* @param throwable
*/
@AfterThrowing(pointcut = POINTCUT, throwing = "throwable")
public void doAfterThrowing(Throwable throwable) {
// 保存異常日誌記錄
log.error("發生異常時間 : {}", LocalDateTime.now());
log.error("拋出異常 : {}", throwable.getMessage());
}
}
com.leilei.web.controller
爲我自己項目定義的切點路徑 那麼當用戶訪問此路徑下所有Controller 以及之中的方法的時候,就是執行我LogAop 切點類之中的各個切點方法
@Before
和@AfterReturning
這部分的代碼也可使用一個環繞通知@Around
來替換
@Around(POINTCUT)
public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取當前的HttpServletRequest對象
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//獲取請求頭中的User-Agent
UserAgent userAgent = UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
//打印請求的內容
startTime = System.currentTimeMillis();
// 請求信息
// 獲取請求類名和方法名稱
Signature signature = joinPoint.getSignature();
// 獲取真實的方法對象
MethodSignature methodSignature = (MethodSignature) signature;
Method method = methodSignature.getMethod();
log.info("請求開始時間:{}" , LocalDateTime.now());
log.info("請求Url : {}" , request.getRequestURL().toString());
log.info("請求方式 : {}" , request.getMethod());
log.info("請求ip : {}" , request.getRemoteAddr());
log.info("請求內容類型 : {}", request.getContentType());
log.info("請求參數 : {}" , Arrays.toString(joinPoint.getArgs()));
// 系統信息
log.info("瀏覽器 : {}", userAgent.getBrowser().toString());
log.info("瀏覽器版本 : {}", userAgent.getBrowserVersion());
log.info("操作系統: {}", userAgent.getOperatingSystem().toString());
// joinPoint.proceed():當我們執行完切面代碼之後,還有繼續處理業務相關的代碼。proceed()方法會繼續執行業務代碼,並且其返回值,就是業務處理完成之後的返回值。
Object result = joinPoint.proceed();
log.info("請求結束時間:"+ LocalDateTime.now());
log.info("請求耗時:{}" , (System.currentTimeMillis() - startTime));
// 處理完請求,返回內容
log.info("請求返回 : " , result);
return result;
}
3.測試
我請求項目com.leilei.web.controller
路徑下的一個方法,然後再IDEA中查看是否打印日誌
那麼,本文到這裏就結束了。。。。。。