使用 SLF4J MDC 给日志添加上下文信息
SLF4J MDC(Mapped Diagnostic Context)可以帮助在日志中添加上下文信息,从而更好地跟踪和调试应用程序。MDC 允许你将特定于线程的键值对存储在日志上下文中,便于在日志中输出相关信息。
使用步骤
-
添加依赖:确保你的项目中已经包含了 SLF4J 和相关的日志实现(如 Logback 或 Log4j)。
-
设置 MDC:在代码中使用
MDC.put(key, value)
设置上下文信息。 -
记录日志:在日志消息中使用
%X
输出 MDC 中的内容。 -
清理 MDC:在处理完成后,使用
MDC.clear()
清理上下文,以避免内存泄漏。
示例代码
以下是一个简单的示例,展示如何使用 SLF4J MDC:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class MdcExample {
private static final Logger logger = LoggerFactory.getLogger(MdcExample.class);
public static void main(String[] args) {
// 设置 MDC 值
MDC.put("userId", "12345");
MDC.put("transactionId", "abc-xyz");
logger.info("This is a log message with MDC context");
// 清理 MDC
MDC.clear();
}
}
日志输出
假设你使用 Logback,日志配置文件中可以定义输出格式如下:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg %X%n</pattern>
</encoder>
</appender>
<root level="info">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
输出结果
运行上述代码后,日志输出可能类似于:
2024-10-27 02:57:00 [main] INFO MdcExample - This is a log message with MDC context {userId=12345, transactionId=abc-xyz}
总结
通过使用 SLF4J MDC,你可以在日志中轻松地添加上下文信息,有助于在多线程环境中追踪问题。记得在使用完 MDC 后清理它,以保持良好的内存管理。
另,MDC 一般配合 AOP / Filter / Interceptor 使用:
@Around(value = "execution(* com.xx.xx.facade.impl.*.*(..))", argNames="pjp")
public Object validator(ProceedingJoinPoint pjp) throws Throwable {
try {
MDC.put("userId", "12345");
MDC.put("transactionId", "abc-xyz");
return pjp.proceed(args);
} catch(Throwable e) {
// 处理错误
} finally {
MDC.clear();
}
}
源码
此处以 Logback 中的实现为例,只分析 MDC 的 put() 方法:
public class MDC {
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also "
+ NULL_MDCA_URL);
}
mdcAdapter.put(key, val);
}
}
MDC 的 put() 方法利用 MDCAdapter 实现。
下面看一下 Logback 中 MDCAdapter 的实现 LogbackMDCAdapter:
public final class LogbackMDCAdapter implements MDCAdapter {
final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal();
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
} else {
Map<String, String> oldMap = (Map)this.copyOnInheritThreadLocal.get();
Integer lastOp = this.getAndSetLastOperation(1);
if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {
oldMap.put(key, val);
} else {
Map<String, String> newMap = this.duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
}
}
}
}
- 如上,LogbackMDCAdapter 有泛型为
Map<String, String>
的 InheritableThreadLocal,Map<String, String>
被用来存储和当前线程相关的上下文信息,MDC 的 put() 方法会将值放在和当前线程关联的 Map 中。 - MDC 内的键值对要能在调用链路中都能打印,那么 Map 肯定是存储在 ThreadLocal 中传递,从代码可以看到为 InheritableThreadLocal。
Map<String, String>
存储在 InheritableThreadLocal 中,AOP 内真正的业务方法内部若进行了子线程的创建,MDC 内的键值对也能正常打印到日志中。但内部若是使用线程池的方式执行细分业务,则线程池任务内打印的日志则不会有此内容(线程池的 ThreadLocal 传递可以用阿里的 TransmittableThreadLocal)。
参考:ChatGPT、slf4j MDC 是个好东西