1.起源:
(1)業(yè)務(wù)需求:
代碼的功能是寫一個(gè)api接口,完成與mysql的crud操作。
后臺使用SpringBoot+JdbcTemplate實(shí)現(xiàn),并在controller層添加了一些請求、響應(yīng)、異常處理的日志;
(2)問題產(chǎn)生:
由于該api接口是多方調(diào)用的,當(dāng)高并發(fā)場景調(diào)用這個(gè)api時(shí),從controller層一路下來,日志的記錄將會非?;靵y,所有業(yè)務(wù)日志都摻雜在一起。比如像下面這樣:

(3)一種解決方法:
當(dāng)產(chǎn)生異常日志時(shí),特別是高并發(fā)的情況下,作為服務(wù)端,你并不知道某條error日志報(bào)的錯(cuò)對應(yīng)的是哪條請求,除非將請求信息打印在日志中,但是仔細(xì)想想,這就需要所有需要添加日志的類都帶著請求信息,顯然這太難做到了,同時(shí)日志文件也將不堪重負(fù)。
2.更好的解決方案
有沒有想到過日志跟蹤呢?如果不管是誰調(diào)用接口,在其每次調(diào)用的時(shí)候,代碼都自動加上一個(gè)唯一的id該多好,不管這次請求是到了service層、dao層、甚至bean層,只要是調(diào)用controller里面的接口,所有這條請求的日志流都加上了這個(gè)唯一的id,這樣再查找日志的時(shí)候,將會很方便。
而怎么才能在不影響已經(jīng)寫好業(yè)務(wù)邏輯的情況下,對日志流做統(tǒng)一處理呢?當(dāng)然是要想到AOP了~!
具體操作如下:
(1)先配上aop的maven依賴(已經(jīng)有aop就不用再加了),這里要注意spring-aop的版本對應(yīng)上spring-core的版本:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
(2)再添加AOP處理類~!這里只針對controller層。(不太了解aop配置的需要補(bǔ)習(xí)一下基本的配置方法)
package com.tools.pincollect.common;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.UUID;
@Aspect
@Configuration
public class SpringAOP {
private static final Logger logger = LoggerFactory.getLogger(SpringAOP.class);
/**
* 定義切點(diǎn)Pointcut
* 第一個(gè)*號:表示返回類型, *號表示所有的類型
* 第二個(gè)*號:表示類名,*號表示所有的類
* 第三個(gè)*號:表示方法名,*號表示所有的方法
* 后面括弧里面表示方法的參數(shù),兩個(gè)句點(diǎn)表示任何參數(shù)
*/
@Pointcut("execution(* com.tools.pincollect.controller.*.*(..))")
public void executionService() {}
/**
* 方法調(diào)用之前調(diào)用
* @param joinPoint
*/
@Before(value = "executionService()")
public void doBefore(JoinPoint joinPoint){
String requestId = String.valueOf(UUID.randomUUID());
MDC.put("requestId",requestId);
logger.info("=====>@Before:請求參數(shù)為:{}",Arrays.toString(joinPoint.getArgs()));
}
/**
* 方法之后調(diào)用
* @param joinPoint
* @param returnValue 方法返回值
*/
@AfterReturning(pointcut = "executionService()",returning="returnValue")
public void doAfterReturning(JoinPoint joinPoint,Object returnValue){
logger.info("=====>@AfterReturning:響應(yīng)參數(shù)為:{}",returnValue);
// 處理完請求,返回內(nèi)容
MDC.clear();
}
}
(3)在項(xiàng)目resources目錄下的logback-spring.xml中修改日志pattern(不了解的話,這里需要補(bǔ)充下日志配置的知識),添加[%X{requestId}],requestId就是在切面類中的MCD.put()方法中的key,如下圖所示:
<pattern>
[ %-5level] [%date{yyyy-MM-dd HH:mm:ss}] [%X{requestId}] %logger{96} [%line] - %msg%n
</pattern>

有了跟蹤id,查找與定位日志是不是方便多了呢?
3.究其原理
依賴的是log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能MDC(Mapped Diagnostic Context,映射調(diào)試上下文)。
MDC 可以看成是一個(gè)與當(dāng)前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內(nèi)容可以被同一線程中執(zhí)行的代碼所訪問。當(dāng)前線程的子線程會繼承其父線程中的 MDC 的內(nèi)容。當(dāng)需要記錄日志時(shí),只需要從 MDC 中獲取所需的信息即可。MDC 的內(nèi)容則由程序在適當(dāng)?shù)臅r(shí)候保存進(jìn)去。對于一個(gè) Web 應(yīng)用來說,通常是在請求被處理的最開始保存這些數(shù)據(jù)。
具體內(nèi)容請參考:
https://blog.csdn.net/sunzhenhua0608/article/details/29175283