利用 AOP 記錄介面日誌

語言: CN / TW / HK

早期文章

常見的小需求

在我們的後端專案中有很多要呼叫第三方介面的地方,而呼叫介面就免不了會因為傳遞給介面的引數有問題報錯,或者對介面的返回值處理不全導致報錯或後續的流程有問題。

對於除錯介面通常的做法就是把入參用介面工具向介面地址提交,然後把獲取到的返回值和專案中的返回值進行比對。這樣來看,在程式碼中呼叫介面的入參和呼叫介面後的返回值對於排錯來說就非常重要了。那這樣的話,我們可以在每個呼叫介面地址的前後使用輸出日誌的方式來記錄,就可以得到呼叫介面的入參和介面的返回值,從而有利於我們以後的除錯了。

我們可以使用 SLF4J 或者 LogBack 等日誌框架,在呼叫介面時來輸出一下入參和返回值,大致方法是在呼叫介面前呼叫 logger.info 輸出入參,然後呼叫介面後再次呼叫 logger.info 輸出返回值。這樣的方式雖然沒有問題,但是在每個介面呼叫前後都要加這樣的日誌輸出程式碼顯得過於麻煩,且不優雅。

簡單的解決方法

在 Spring 框架中為我們提供了 AOP,即面向切面程式設計。AOP 通過動態代理來管理切面環境,通過反射可以使我們在非侵入的方式下為我們增加前置、後置等方法用來貫穿整個程式碼層面,從而讓我們更加關注業務本身的開發。在 Spring 中的事務就是通過 AOP 來進行管理的,我們這裡通過 AOP 完成一個介面呼叫時列印入參和返回值的功能。

AOP 有一些名詞需要理解,但是不理解這些名詞好像又不影響我們實際 AOP 的使用。這些名詞包括,切面、通知、引入、切點、連線點和織入。這裡我們不討論這些名詞,直接上程式碼來進行演示。

程式碼演示

首先引入依賴,依賴如下:

<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>

然後,我們來定義一個切面,所謂的切面不過是一個被 @Aspect 註解修飾的類,在類中可以定義一些通知,通知通常包含(前置通知、後置通知、返回通知、異常通知等) ,這裡我們定義了前置通知和返回通知。通知是針對切點進行的,切點是用於針對的具體的方法,我們切面類如下:

@Aspect
@Component
@Slf4j
public class TestAspect {


@Pointcut("execution(public * io.coderup2u.xxx.util.yyy.*.*(..))")
public void controllerMethod() {}


@Before("controllerMethod()")
public void before(JoinPoint joinPoint) {


}


@AfterReturning(value = "controllerMethod()", returning = "methodResult")
public void afterReturning(JoinPoint joinPoint, Object methodResult) {


}
}

         在上面的程式碼中, 使用 @Before 和 @AfterReturning 定義了兩個通知,分別是前置通知和返回通知 使用 @Pointcut 定義了一個切點,通過 execution 的正則表示式來確定一個連線點(所謂的連線點就是我們實際的業務類) ,這裡 execution 正則表示式的意思是,當執行 io.coderup2u.xxx.util.yyy 下的所有方法時,都會執行切面中的前置通知和返回通知。 前置方法是在業務方法執行前被執行,返回通知是在業務方法執行後且沒有異常時執行。

         在 before 和 afterReturning 方法中都有一個 JoinPoint 型別的引數,通過該引數可以得到被執行具體方法的名稱以及引數,afterReturning 方法的 methodResult 引數可以得到方法執行後的返回值。 具體程式碼如下:

@Pointcut("execution(public * io.coderup2u.xxx.util.yyy.*.*(..))")
public void controllerMethod() {}


@Before("controllerMethod()")
public void before(JoinPoint joinPoint)
{
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();


long tid = Thread.currentThread().getId();


log.info("===> TID:{} => 準備呼叫 {} 方法", tid, method.getName());


if (joinPoint.getArgs().length == 0) {
return ;
}


log.info("===> TID:{} => 它的引數如下:", tid);
for (int i = 0; i < joinPoint.getArgs().length; i ++) {
Object arg = joinPoint.getArgs()[i];
log.info("===> TID:{} => 第 {} 個引數是:{}", tid, i + 1, arg.toString());
}
}


@AfterReturning(value = "controllerMethod()", returning = "methodResult")
public void afterReturning(JoinPoint joinPoint, Object methodResult)
{
MethodSignature ms = (MethodSignature) joinPoint.getSignature();
Method method = ms.getMethod();


long tid = Thread.currentThread().getId();


log.info("<=== TID:{} => 方法: {} 的返回值為: {}", tid, method.getName(), methodResult.toString());
}

最後呼叫 一個介面,來看下 AOP 記錄的日誌,日誌如下:

===> TID:40 => 準備呼叫 getAccessToken 方法
<=== TID:40 => 方法: getAccessToken 的返回值為: {"code":0,"data":{"accessToken":"xxxxxxxxxxxx","expiresIn":7200},"message":"成功"}

可以看到上面的輸出,幫我們輸出了執行緒的 ID,也輸出了呼叫的方法名和方法的返回結果。

公眾號內回覆 【mongo】 下載 SpringBoot 整合操作 MongoDB 的文件。

公眾號內回覆 【 cisp知識整理 】 下載 CISP 讀書筆記。

公眾號內 回覆【java開發手冊】獲取《Java開發手冊》黃山版。