MDC+Filter/Interceptor:用戶信息日志追蹤
前言
在分布式系統或復雜業務系統中,日志是排查問題、追蹤業務流程的核心工具。但默認日志往往缺乏用戶維度的關聯信息,當出現問題時難以快速定位某個用戶的操作鏈路。本文將詳細介紹如何通過Filter(過濾器) 或 Interceptor(攔截器) 結合日志框架的MDC機制,實現用戶信息的自動注入與日志追蹤。
原理解析
在實現用戶信息追蹤前,需先理解三個核心技術組件的作用與協作邏輯:MDC、Filter、Interceptor。
MDC:日志上下文的容器
MDC 是 SLF4J及 Logback、Log4j2等日志框架提供的上下文工具,本質是基于ThreadLocal實現的線程級別的鍵值對存儲容器。其核心作用是:
- 在請求處理線程中存儲臨時上下文信息(如用戶ID、用戶名、請求ID 等);
- 日志輸出時自動從MDC中提取配置的鍵值,無需在每處日志打印代碼中手動傳入用戶信息;
- 線程結束后自動清理上下文,避免內存泄漏(需手動保證清理邏輯)。
MDC核心API(以SLF4J為例):
// 向MDC中存入鍵值對(如用戶ID)
MDC.put("userId", "user_123456");
// 從MDC中獲取值
String userId = MDC.get("userId");
// 清空當前線程的MDC上下文(關鍵,必須執行)
MDC.clear();Filter 與 Interceptor:請求鏈路的攔截器
圖片

Filter和Interceptor均用于攔截HTTP請求,在請求處理前后執行自定義邏輯(如權限校驗、參數預處理),是注入MDC上下文的最佳切入點。
實踐方案
方案 1:基于 Filter+MDC 實現
自定義 MDC 過濾器
/**
* 基于Filter的MDC用戶信息注入過濾器
*/
public class MdcUserFilter extends OncePerRequestFilter {
// 定義MDC中用戶信息的鍵(需與日志配置一致)
private static final String MDC_USER_ID = "userId";
private static final String MDC_USER_NAME = "userName";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 1. 從請求頭提取用戶信息(實際項目需替換為Token解析、Session獲取等邏輯)
String userId = request.getHeader("X-User-Id");
String userName = request.getHeader("X-User-Name");
// 2. 注入MDC(若用戶未登錄,可存入默認值如"unknown")
MDC.put(MDC_USER_ID, userId != null ? userId : "unknown");
MDC.put(MDC_USER_NAME, userName != null ? userName : "unknown");
// 3. 繼續執行請求鏈路(進入Controller層)
filterChain.doFilter(request, response);
} finally {
// 4. 關鍵:請求結束后清空MDC,避免線程復用導致上下文污染(線程池場景必做)
MDC.clear();
}
}
}步驟 2:注冊 Filter 到 Spring 容器
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<MdcUserFilter> mdcUserFilterRegistration() {
FilterRegistrationBean<MdcUserFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new MdcUserFilter());
// 攔截所有請求
registrationBean.addUrlPatterns("/*");
// 設置過濾器優先級(值越小優先級越高,確保先于其他業務過濾器執行)
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
}方案 2:基于 Interceptor+MDC 實現
步驟 1:自定義 MDC 攔截器
/**
* 基于Interceptor的MDC用戶信息注入攔截器
*/
public class MdcUserInterceptor implements HandlerInterceptor {
private static final String MDC_USER_ID = "userId";
private static final String MDC_USER_NAME = "userName";
/**
* 請求處理前執行:注入MDC
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 提取用戶信息(邏輯與Filter一致,可復用工具類)
String userId = request.getHeader("X-User-Id");
String userName = request.getHeader("X-User-Name");
// 2. 注入MDC
MDC.put(MDC_USER_ID, userId != null ? userId : "unknown");
MDC.put(MDC_USER_NAME, userName != null ? userName : "unknown");
// 返回true:繼續執行后續鏈路(如Controller)
returntrue;
}
/**
* 請求完成后執行(無論是否拋異常):清空MDC
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 關鍵:清空MDC,避免線程池上下文污染
MDC.clear();
}
}步驟 2:注冊 Interceptor 到 Spring MVC
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MdcUserInterceptor())
// 攔截所有請求
.addPathPatterns("/**")
// 排除無需攔截的路徑(如登錄接口、靜態資源)
.excludePathPatterns("/api/login", "/static/**", "/error");
}
}配置日志輸出 MDC 信息
<configuration>
<!-- 控制臺輸出配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 日志格式:時間 [線程名] 日志級別 類名 - 用戶ID:xxx 用戶名:xxx 日志內容 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - userId:%X{userId} userName:%X{userName} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件輸出配置(按天滾動) -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/app.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory> <!-- 保留30天日志 -->
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - userId:%X{userId} userName:%X{userName} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 全局日志級別:INFO -->
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<!-- 業務包日志級別:DEBUG(按需調整) -->
<logger name="com.example.business" level="DEBUG" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</logger>
</configuration>線程池場景下的 MDC 傳遞
手動傳遞 MDC 上下文
public class AsyncMdcDemo {
// 初始化線程池
private static final ExecutorService executorService = Executors.newFixedThreadPool(5);
public void doAsyncTask() {
// 1. 獲取當前線程的MDC上下文(包含用戶信息)
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
// 2. 提交異步任務到線程池
executorService.submit(() -> {
try {
// 3. 子線程中注入MDC上下文
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
// 4. 異步業務邏輯(日志會自動包含用戶信息)
log.info("執行異步任務:處理用戶訂單");
} finally {
// 5. 子線程結束后清空MDC
MDC.clear();
}
});
}
}封裝線程池(避免重復代碼)
// Spring異步任務裝飾器:自動傳遞MDC
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// 獲取當前線程MDC上下文
Map<String, String> mdcContext = MDC.getCopyOfContextMap();
return () -> {
try {
// 子線程注入MDC
if (mdcContext != null) {
MDC.setContextMap(mdcContext);
}
runnable.run();
} finally {
MDC.clear();
}
};
}
}
// 配置Spring異步線程池(使用裝飾器)
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(25);
// 設置MDC裝飾器
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
}總結
通過Filter/Interceptor+MDC實現用戶信息追蹤,核心是 攔截請求→注入上下文→日志輸出→清理上下文 的閉環流程,其價值在于:
- 無侵入式:無需在業務代碼中手動傳遞用戶信息到日志,降低開發成本;
- 可追溯性:日志自動關聯用戶維度,快速定位單個用戶的操作鏈路;
- 靈活性:支持全局或局部攔截,適配不同業務場景。






























