SpringBoot中全局日志記錄與請(qǐng)求鏈路追蹤實(shí)戰(zhàn)教學(xué)
在微服務(wù)和高并發(fā)架構(gòu)下,日志是排查問(wèn)題的“救命稻草”。本文將講解如何利用 AOP、MDC 等技術(shù),構(gòu)建一套包含請(qǐng)求追蹤、出入?yún)⒂涗?、敏感信息脫敏的全局日志系統(tǒng)。

一、 為什么需要全局日志和鏈路追蹤?
想象一下這樣的場(chǎng)景:用戶反饋下單失敗,客服給你一個(gè)訂單號(hào)。你開(kāi)始查日志…
- 如果不帶唯一標(biāo)識(shí):你需要在一堆雜亂的并發(fā)請(qǐng)求中找到屬于那個(gè)用戶的那一次請(qǐng)求,猶如大海撈針。
- 如果接口多:如果涉及多個(gè)服務(wù)調(diào)用,你需要跨機(jī)器拼接日志片段。
為了解決這些問(wèn)題,我們需要:
- 鏈路追蹤 (TraceId) :為每個(gè)請(qǐng)求生成唯一的 ID,貫穿整個(gè)處理流程,串聯(lián)起所有相關(guān)日志。
- 全局日志 :自動(dòng)記錄接口入?yún)ⅰ⒊鰠?、耗時(shí),減少手動(dòng)寫(xiě)
log.info(...)的工作量。
二、 核心組件:MDC (Mapped Diagnostic Context)
MDC 是 Slf4j 提供的一個(gè)機(jī)制,它實(shí)際上是一個(gè)線程安全的 ThreadLocal<Map>。我們可以往里面放入鍵值對(duì)(如 TraceId),然后在 Logback 配置中通過(guò) %X{key} 的格式輸出。
1. 生成 TraceId
我們需要一個(gè)工具類來(lái)管理 TraceId。
public class TraceIdUtil {
private static final String TRACE_ID_KEY = "traceId";
/**
* 獲取當(dāng)前線程的 TraceId
*/
public static String getTraceId() {
String traceId = MDC.get(TRACE_ID_KEY);
return StringUtils.isEmpty(traceId) ? "" : traceId;
}
/**
* 設(shè)置 TraceId
*/
public static void setTraceId(String traceId) {
MDC.put(TRACE_ID_KEY, traceId);
}
/**
* 清除 TraceId (非常重要,防止線程池復(fù)用導(dǎo)致數(shù)據(jù)污染)
*/
public static void removeTraceId() {
MDC.remove(TRACE_ID_KEY);
}
}
2. 通過(guò) Filter 攔截請(qǐng)求并設(shè)置 TraceId
在請(qǐng)求進(jìn)入 Controller 之前,我們通過(guò)一個(gè) Filter 來(lái)生成并設(shè)置 TraceId。
@Slf4j
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 確保最先執(zhí)行
public class TraceIdFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
try {
// 嘗試從請(qǐng)求頭獲取 TraceId(適用于網(wǎng)關(guān)傳遞過(guò)來(lái)的場(chǎng)景)
String traceId = httpRequest.getHeader("X-Trace-Id");
if (StringUtils.isEmpty(traceId)) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
TraceIdUtil.setTraceId(traceId);
chain.doFilter(request, response);
} finally {
// 務(wù)必清理,防止內(nèi)存泄漏或線程池復(fù)用導(dǎo)致的臟數(shù)據(jù)
TraceIdUtil.removeTraceId();
}
}
}
3. 配置 Logback 輸出格式
修改 logback-spring.xml,在 pattern 中加入 %X{traceId}:
<configuration>
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n"/>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>現(xiàn)在,你的日志輸出將類似這樣:2023-10-01 12:00:00.001 [http-nio-8080-exec-1] [a1b2c3d4e5f6...] INFO c.e.controller.UserController - User created successfully
三、 AOP 實(shí)現(xiàn)全局接口日志
雖然 MDC 幫我們串聯(lián)了日志,但我們還需要知道每個(gè)接口的入?yún)?、出參和耗時(shí)。手動(dòng)打印太麻煩,使用 AOP (Aspect Oriented Programming) 是最好的選擇。
1. 引入依賴與啟用 AOP
確保引入了 spring-boot-starter-aop。
2. 編寫(xiě)日志切面
我們定義一個(gè)切面,攔截所有 Controller 層的 public 方法。
@Slf4j
@Aspect
@Component
public class ControllerLogAspect {
// 定義切點(diǎn):攔截 Controller 包下的所有方法
@Pointcut("execution(public * com.example..controller..*.*(..))")
public void controllerLog() {}
@Around("controllerLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 獲取請(qǐng)求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 構(gòu)建日志對(duì)象
LogInfo logInfo = new LogInfo();
logInfo.setTraceId(TraceIdUtil.getTraceId());
logInfo.setMethod(request.getMethod());
logInfo.setUrl(request.getRequestURI());
logInfo.setIp(request.getRemoteAddr());
logInfo.setClassMethod(joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
// 獲取入?yún)?
Object[] args = joinPoint.getArgs();
// 注意:過(guò)濾掉 HttpServletRequest, HttpServletResponse 等無(wú)法序列化的參數(shù)
Object[] logArgs = Arrays.stream(args)
.filter(arg -> !(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse))
.toArray();
logInfo.setArgs(logArgs);
// 打印入?yún)⑷罩?
log.info("Request Info: {}", JSON.toJSONString(logInfo));
Object result;
try {
result = joinPoint.proceed();
} catch (Exception e) {
// 記錄異常日志(通常由全局異常處理器處理,這里也可以記錄一份用于審計(jì))
log.error("Controller Exception: ", e);
throw e;
}
// 計(jì)算耗時(shí)
long costTime = System.currentTimeMillis() - startTime;
logInfo.setCostTime(costTime);
logInfo.setResult(result);
// 打印出參日志
log.info("Response Info ({}ms): {}", costTime, JSON.toJSONString(logInfo));
return result;
}
@Data
public static class LogInfo {
private String traceId;
private String method;
private String url;
private String ip;
private String classMethod;
private Object[] args;
private Object result;
private Long costTime;
}
}
四、 敏感信息脫敏
在實(shí)際生產(chǎn)環(huán)境中,日志里絕對(duì)不能出現(xiàn)明文密碼、身份證號(hào)、手機(jī)號(hào)等敏感信息。我們可以在序列化(如 Jackson 或 Fastjson)輸出日志前進(jìn)行處理。
自定義脫敏序列化器 (以 Jackson 為例)
public class SensitiveDataSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (StringUtils.isEmpty(value)) {
gen.writeNull();
return;
}
// 簡(jiǎn)單的脫敏邏輯:保留前 3 后 4,中間星號(hào)
if (value.length() > 8) {
gen.writeString(value.substring(0, 3) + "****" + value.substring(value.length() - 4));
} else {
gen.writeString("****");
}
}
}
使用方式:在 DTO 的敏感字段上添加注解
public class UserDTO {
@JsonSerialize(using = SensitiveDataSerializer.class)
private String phone;
// ...
}
或者在 AOP 中通過(guò)正則替換整個(gè) JSON 字符串后再打印。
五、 最佳實(shí)踐
- 異步日志 (AsyncAppender) :生產(chǎn)環(huán)境日志量巨大,同步打印日志會(huì)阻塞業(yè)務(wù)線程。務(wù)必在 Logback 配置中使用
AsyncAppender包裝Console和FileAppender。 - 線程池透?jìng)?MDC :如果你的代碼使用了
@Async或自定義線程池,MDC 的值不會(huì)自動(dòng)傳遞到子線程。你需要實(shí)現(xiàn)TaskDecorator或ThreadPoolTaskExecutor的包裝類,在提交任務(wù)時(shí)拷貝 MDC 上下文,執(zhí)行完畢后恢復(fù)。 - 避免打印大對(duì)象 :如果入?yún)笪募鳎∕ultipartFile)或巨大的列表,不要直接打印 JSON,會(huì)撐爆日志文件。在 AOP 中判斷參數(shù)類型進(jìn)行過(guò)濾。
- 日志級(jí)別控制 :不要把所有日志都設(shè)為 INFO。開(kāi)發(fā)環(huán)境可以是 DEBUG,生產(chǎn)環(huán)境建議 INFO 或 WARN,Error 必須報(bào)警。
六、 總結(jié)
通過(guò) MDC + Filter + AOP 的組合拳,我們實(shí)現(xiàn)了:
- 全鏈路追蹤:每個(gè)請(qǐng)求都有唯一的 TraceId。
- 自動(dòng)審計(jì) :無(wú)需修改業(yè)務(wù)代碼,自動(dòng)記錄接口出入?yún)⒑秃臅r(shí)。
- 數(shù)據(jù)安全 :敏感信息脫敏,避免合規(guī)風(fēng)險(xiǎn)。
這套方案是構(gòu)建生產(chǎn)級(jí) SpringBoot 應(yīng)用的標(biāo)配,掌握它將為你的系統(tǒng)可觀測(cè)性打下堅(jiān)實(shí)基礎(chǔ)。
到此這篇關(guān)于SpringBoot中全局日志記錄與請(qǐng)求鏈路追蹤實(shí)戰(zhàn)教學(xué)的文章就介紹到這了,更多相關(guān)SpringBoot全局日志系統(tǒng)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)十秒向MySQL插入百萬(wàn)條數(shù)據(jù)
這篇文章主要為大家詳細(xì)介紹了Java如何實(shí)現(xiàn)十秒向MySQL插入百萬(wàn)條數(shù)據(jù),文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)或工作有一定借鑒價(jià)值,需要的可以參考一下2022-11-11
springboot對(duì)壓縮請(qǐng)求的處理方法
這篇文章主要介紹了springboot對(duì)壓縮請(qǐng)求的處理,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05
淺談Java(SpringBoot)基于zookeeper的分布式鎖實(shí)現(xiàn)
這篇文章主要介紹了Java(SpringBoot)基于zookeeper的分布式鎖實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
IntelliJ IDEA 常用設(shè)置(配置)吐血整理(首次安裝必需)
這篇文章主要介紹了IntelliJ IDEA 常用設(shè)置(配置)吐血整理(首次安裝必需),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
Java Socket循環(huán)接收數(shù)據(jù)readLine()阻塞的解決方案
這篇文章主要介紹了Java Socket循環(huán)接收數(shù)據(jù)readLine()阻塞的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-08-08
SpringBoot實(shí)現(xiàn)自定義指標(biāo)監(jiān)控功能
本文主要介紹了SpringBoot實(shí)現(xiàn)自定義指標(biāo)監(jiān)控功能的實(shí)現(xiàn),,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,感興趣的小伙伴跟著著小編來(lái)一起來(lái)學(xué)習(xí)吧2024-01-01
Java實(shí)現(xiàn)異步執(zhí)行的8種方式總結(jié)
這篇文章主要給大家介紹了關(guān)于Java實(shí)現(xiàn)異步執(zhí)行的8種方式,異步編程不會(huì)阻塞程序的執(zhí)行,它將耗時(shí)的操作提交給后臺(tái)線程或其他執(zhí)行環(huán)境,并立即返回,使得程序可以繼續(xù)執(zhí)行其他任務(wù),需要的朋友可以參考下2023-09-09
SpringBoot整合Swagger框架過(guò)程解析
這篇文章主要介紹了SpringBoot整合Swagger框架過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05

