0
点赞
收藏
分享

微信扫一扫

Slf4j MDC详解

Mhhao 2021-09-28 阅读 168

1、简介

MDC 全拼 Mapped Diagnostic Contexts,是SLF4J类日志系统中实现分布式多线程日志数据传递的重要工具(不同的系统有不同的实现方式,下文中会有介绍);同时,用户也可利用MDC将一些运行时的上下文数据打印出来。

2、应用

自定义日志输出内容:用户可以将某个或某些所有日志中都需要打印的字符串设置于MDC中,这样就不需要每次打印日志时都专门写出来。例如:调用链系统中调用链唯一标示 traceID 及其中某次调用关系标示rpcID

3、官方介绍

看一下logback中关于MDC的介绍:

大意为:现今分布式系统大多有多个客户端,使用多线程提供服务。为了在日志输出中体现出来自不同的客户端,一个轻量但不被鼓励的方式是为每个不同的客户端设置不同的日志。这种技术毫无疑问会极大的增加管理成本。

大意为:另一个轻量级的实现方式是唯一的标示每个客户端发来的请求。Logback利用了SLF4J API中包含的一种技术:Mapped Diagnostic Contexts (MDC)。

看一下MDC类本身的注解:

大意为:目前只有log4j和logback提供原生的MDC支持,其他的会有其他的实现方式。

4、实现方式

此处以 Log4j2 中的实现为例。为了方便讲解,我们只分析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实现。
下面看一下Log4j中MDCAdapter的实现Log4jMDCAdapter

public class Log4jMDCAdapter implements MDCAdapter {
    
    public void put(String key, String val) {
        ThreadContext.put(key, val);
    }

}
public final class ThreadContext {
    
    ......
    private static ThreadContextMap contextMap;
    
    private ThreadContext() {
    }

    static void init() {
        contextMap = null;
        ......
        if(!useMap) {
            contextMap = new NoOpThreadContextMap();
        } else {
            contextMap = ThreadContextMapFactory.createThreadContextMap();
        }

    }

    public static void put(String key, String value) {
        contextMap.put(key, value);
    }

public static ThreadContextMap createThreadContextMap() {
        // 此处获取用户自定义ThreadContextMap
        final String threadContextMapName = managerProps.getStringProperty(THREAD_CONTEXT_KEY);
        ......(其他代码)
        // 若无特殊设置,将使用默认ThreadContextMap
        if (result == null) {
            result = createDefaultThreadContextMap();
        }
        return result;
    }

ThreadContextMapFactory 中 createDefaultThreadContextMap() 的定义:

    private static ThreadContextMap createDefaultThreadContextMap() {
        return (ThreadContextMap)(Constants.ENABLE_THREADLOCALS?(PropertiesUtil.getProperties().getBooleanProperty("log4j2.garbagefree.threadContextMap")?new GarbageFreeSortedArrayThreadContextMap():new CopyOnWriteSortedArrayThreadContextMap()):new DefaultThreadContextMap(true));
    }
class GarbageFreeSortedArrayThreadContextMap implements ThreadContextMap2 {
    public static final String INHERITABLE_MAP = "isThreadContextMapInheritable";
    protected static final int DEFAULT_INITIAL_CAPACITY = 16;
    protected static final String PROPERTY_NAME_INITIAL_CAPACITY = "log4j2.ThreadContext.initial.capacity";
    //注意如下定义
    protected final ThreadLocal<StringMap> localMap = this.createThreadLocalMap();

    private ThreadLocal<StringMap> createThreadLocalMap() {
        PropertiesUtil managerProps = PropertiesUtil.getProperties();
        boolean inheritable = managerProps.getBooleanProperty("isThreadContextMapInheritable");
        return (ThreadLocal)(inheritable?new InheritableThreadLocal<StringMap>() {
            protected StringMap childValue(StringMap parentValue) {
                return parentValue != null?GarbageFreeSortedArrayThreadContextMap.this.createStringMap(parentValue):null;
            }
        }:new ThreadLocal());
    }
    ......
}

经过上述代码追踪可以看到,Log4j中定义了一个ThreadLocal,利用 ThreadLocal 实现不同请求的线程区分。如果对ThreadLocal不太了解,可以阅读ThreadLocal详解。

5、自定义ThreadContextMap

官方文档 中关于用户自定义ThreadContextMap的描述:

让我们再看一下上文提到的createThreadContextMap():

    private static final String THREAD_CONTEXT_KEY = "log4j2.threadContextMap";

    public static ThreadContextMap createThreadContextMap() {
        final PropertiesUtil managerProps = PropertiesUtil.getProperties();
        final String threadContextMapName = managerProps.getStringProperty(THREAD_CONTEXT_KEY);
        final ClassLoader cl = ProviderUtil.findClassLoader();
        ThreadContextMap result = null;
        if (threadContextMapName != null) {
            try {
                final Class<?> clazz = cl.loadClass(threadContextMapName);
                if (ThreadContextMap.class.isAssignableFrom(clazz)) {
                    result = (ThreadContextMap) clazz.newInstance();
                }
            } catch (final ClassNotFoundException cnfe) {
                LOGGER.error("Unable to locate configured ThreadContextMap {}", threadContextMapName);
            } catch (final Exception ex) {
                LOGGER.error("Unable to create configured ThreadContextMap {}", threadContextMapName, ex);
            }
        }
        ......(其他代码)
    }

createThreadContextMap()在开始处首先判断用户是否设置了系统变量log4j2.threadContextMap,若设置了,就开始实例化。
设置log4j2.threadContextMap变量有两种方式:

  1. 项目启动时,命令行中设置:-Dlog4j2.threadContextMap=xxxx
  2. 资源文件根目录下创建 log4j2.component.properties 文件,文件内填写 log4j2.threadContextMap=xxxx

6、总结

经过上述分析可以知道,Log4j中利用ThreadLocal实现MDC,达到不同线程间日志信息互不影响的目的。

举报

相关推荐

0 条评论