使用 SLF4J MDC 给日志添加上下文信息

SLF4J MDC(Mapped Diagnostic Context)可以帮助在日志中添加上下文信息,从而更好地跟踪和调试应用程序。MDC 允许你将特定于线程的键值对存储在日志上下文中,便于在日志中输出相关信息。

使用步骤

  1. 添加依赖:确保你的项目中已经包含了 SLF4J 和相关的日志实现(如 Logback 或 Log4j)。

  2. 设置 MDC:在代码中使用MDC.put(key, value)设置上下文信息。

  3. 记录日志:在日志消息中使用%X输出 MDC 中的内容。

  4. 清理 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 是个好东西

posted @ 2024-10-27 16:48  Higurashi-kagome  阅读(55)  评论(0编辑  收藏  举报