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变量有两种方式:
- 项目启动时,命令行中设置:-Dlog4j2.threadContextMap=xxxx
- 资源文件根目录下创建 log4j2.component.properties 文件,文件内填写 log4j2.threadContextMap=xxxx
6、总结
经过上述分析可以知道,Log4j中利用ThreadLocal实现MDC,达到不同线程间日志信息互不影响的目的。