0
点赞
收藏
分享

微信扫一扫

Slf4j MDC使用与实现

前言

如今,在 Java 开发中,日志的打印输出是必不可少的,Slf4j + LogBack 的组合是最通用的方式。

有了日志之后,我们就可以追踪各种线上问题。但是,在分布式系统中,各种无关日志穿行其中,导致我们可能无法直接定位整个操作流程。因此,我们可能需要对一个用户的操作流程进行归类标记,比如使用线程+时间戳,或者用户身份标识等;如此,我们可以从大量日志信息中grep出某个用户的操作流程,或者某个时间的流转记录。

因此,这就有了 Slf4j MDC 方法。

Slf4j MDC 介绍

MDC ( Mapped Diagnostic Contexts ),顾名思义,其目的是为了便于我们诊断线上问题而出现的方法工具类。虽然,Slf4j 是用来适配其他的日志具体实现包的,但是针对 MDC功能,目前只有logback 以及 log4j 支持,或者说由于该功能的重要性,slf4j 专门为logback系列包装接口提供外部调用(玩笑~:))。

logback 和 log4j 的作者为同一人,所以这里统称logback系列。

先来看看 MDC 对外提供的接口:

public class MDC {
  //Put a context value as identified by key
  //into the current thread's context map.
  public static void put(String key, String val);

  //Get the context identified by the key parameter.
  public static String get(String key);

  //Remove the context identified by the key parameter.
  public static void remove(String key);

  //Clear all entries in the MDC.
  public static void clear();
}

 

接口定义非常简单,此外,其使用也非常简单。

如上代码所示,一般,我们在代码中,只需要将指定的值put到线程上下文的Map中,然后,在对应的地方使用 get方法获取对应的值。此外,对于一些线程池使用的应用场景,可能我们在最后使用结束时,需要调用clear方法来清洗将要丢弃的数据。

然后,看看一个MDC使用的简单示例。

public class LogTest {
    private static final Logger logger = LoggerFactory.getLogger(LogTest.class);

    public static void main(String[] args) {

        MDC.put("THREAD_ID", String.valueOf(Thread.currentThread().getId()));

        logger.info("纯字符串信息的info级别日志");

    }
}

 

 

然后看看logback的输出模板配置:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="log.base" value="${catalina.base}/logs" />

    <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
        <resetJUL>true</resetJUL>
    </contextListener>

    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder charset="UTF-8">
            <pattern>[%d{yyyy-MM-dd HH:mm:ss} %highlight(%-5p) %logger.%M\(%F:%L\)] %X{THREAD_ID} %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="console" />
    </root>
</configuration>

 

于是,就有了输出:

[2015-04-30 15:34:35 INFO  io.github.ketao1989.log4j.LogTest.main(LogTest.java:29)] 1 纯字符串信息的info级别日志

 

当我们在web应用中,对服务的所有请求前进行filter拦截,然后加上自定义的唯一标识到MDC中,就可以在所有日志输出中,清楚看到某用户的操作流程。关于web MDC,会单独一遍博客介绍。

此外,关于logback 是如何将模板中的变量替换成具体的值,会在下一节分析。

在日志模板logback.xml 中,使用 %X{ }来占位,替换到对应的 MDC 中 key 的值。

前置知识介绍

InheritableThreadLocal 介绍
在代码开发中,经常使用 ThreadLocal来保证在同一个线程中共享变量。在 ThreadLocal 中,每个线程都拥有了自己独立的一个变量,线程间不存在共享竞争发生,并且它们也能最大限度的由CPU调度,并发执行。显然这是一种以空间来换取线程安全性的策略。

但是,ThreadLocal有一个问题,就是它只保证在同一个线程间共享变量,也就是说如果这个线程起了一个新线程,那么新线程是不会得到父线程的变量信息的。因此,为了保证子线程可以拥有父线程的某些变量视图,JDK提供了一个数据结构,InheritableThreadLocal。

javadoc 文档对 InheritableThreadLocal 说明:

该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数。

当必须将变量(如用户 ID 和 事务 ID)中维护的每线程属性(per-thread-attribute)自动传送给创建的所有子线程时,应尽可能地采用可继承的线程局部变量,而不是采用普通的线程局部变量。

代码对比可以看出两者区别:
ThreadLocal

public class ThreadLocal<T> {

    /**
     * Method childValue is visibly defined in subclass
     * InheritableThreadLocal, but is internally defined here for the
     * sake of providing createInheritedMap factory method without
     * needing to subclass the map class in InheritableThreadLocal.
     * This technique is preferable to the alternative of embedding
     * instanceof tests in methods.
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }
}

 

InheritableThreadLocal

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

Slf4j MDC使用与实现_代码实现

 

// 这个是开发时一般使用的类,直接copy父线程的变量

public class CopyOnInheritThreadLocal extends
    InheritableThreadLocal<HashMap<String, String>> {

  /**
   * Child threads should get a copy of the parent's hashmap.
   */
  @Override
  protected HashMap<String, String> childValue(
      HashMap<String, String> parentValue) {
    if (parentValue == null) {
      return null;
    } else {
      return new HashMap<String, String>(parentValue);
    }
  }

}

Slf4j MDC使用与实现_代码实现

 

为了支持InheritableThreadLocal的父子线程传递变量,JDK在Thread中,定义了ThreadLocal.ThreadLocalMap inheritableThreadLocals 属性。该属性变量在线程初始化的时候,如果父线程的该变量不为null,则会把其值复制到ThreadLocal。

从上面的代码实现,还可以看到,如果我们使用原生的 InheritableThreadLocal类则在子线程中修改变量,可能会影响到父线程的变量值,及其他子线程的值。因此,一般我们推荐没有特殊情况,最好使用CopyOnInheritThreadLocal类,该实现是新建一个map来保持值,而不是直接使用父线程的引用。

Log MDC 实现分析

Slf4j MDC 实现分析
Slf4j 的实现原则就是调用底层具体实现类,比如logback,logging等包;而不会去实现具体的输出打印等操作。因此,除了前文中介绍的门面(Facade)模式外,提供这种功能的还有适配器(Adapter)模式和装饰(Decorator)模式。

MDC 使用的就是Decorator模式,虽然,其类命名为M MDCAdapter。

Slf4j MDC 内部实现很简单。实现一个单例对应实例,获取具体的MDC实现类,然后其对外接口,就是对参数进行校验,然后调用 MDCAdapter 的方法实现。

实现源码如下:

public class MDC {

  static MDCAdapter mdcAdapter;

  private MDC() {
  }

  static {
    try {
      mdcAdapter = StaticMDCBinder.SINGLETON.getMDCA();
    } catch (NoClassDefFoundError ncde) {
      //......
  }


  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);
  }

  public static String get(String key) 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);
    }
    return mdcAdapter.get(key);
  }
}  }

Slf4j MDC使用与实现_代码实现

 

对于Slf4j的MDC 部分非常简单,MDC的核心实现是在logback方法中的。

在 logback 中,提供了 LogbackMDCAdapter类,其实现了MDCAdapter接口。基于性能的考虑,logback 对于InheritableThreadLocal的使用做了一些优化工作。

Logback MDC 实现分析
Logback 中基于 MDC 实现了LogbackMDCAdapter 类,其 get 方法实现很简单,但是 put 方法会做一些优化操作。

关于 put 方法,主要有:
使用原始的InheritableThreadLocal<Map<String, String>>类,而不是使用子线程复制类 CopyOnInheritThreadLocal。这样,运行时可以大量避免不必要的copy操作,节省CPU消耗,毕竟在大量log操作中,子线程会很少去修改父线程中的key-value值。

由于上一条的优化,所以代码实现上实现了一个写时复制版本的 InheritableThreadLocal。实现会根据上一次操作来确定是否需要copy一份新的引用map,而不是去修改老的父线程的map引用。

此外,和 log4j 不同,其map中的val可以为null。

下面给出,get 和 put 的代码实现:

public final class LogbackMDCAdapter implements MDCAdapter {

  final InheritableThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new InheritableThreadLocal<Map<String, String>();

  private static final int WRITE_OPERATION = 1;
  private static final int READ_OPERATION = 2;

  // keeps track of the last operation performed
  final ThreadLocal<Integer> lastOperation = new ThreadLocal<Integer>();

  private Integer getAndSetLastOperation(int op) {
    Integer lastOp = lastOperation.get();
    lastOperation.set(op);
    return lastOp;
  }

  private boolean wasLastOpReadOrNull(Integer lastOp) {
    return lastOp == null || lastOp.intValue() == READ_OPERATION;
  }

  private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {
    Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
    if (oldMap != null) {
        // we don't want the parent thread modifying oldMap while we are
        // iterating over it
        synchronized (oldMap) {
          newMap.putAll(oldMap);
        }
    }

    copyOnInheritThreadLocal.set(newMap);
    return newMap;
  }


  public void put(String key, String val) throws IllegalArgumentException {
    if (key == null) {
      throw new IllegalArgumentException("key cannot be null");
    }

    Map<String, String> oldMap = copyOnInheritThreadLocal.get();
    Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);

    if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
      // 当上一次操作是read时,这次write,则需要new
      Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
      newMap.put(key, val);
    } else {
      // 写的话,已经new了就不需要再new
      oldMap.put(key, val);
    }
  }

  /**
   * Get the context identified by the <code>key</code> parameter.
   * <p/>
   */
  public String get(String key) {
    Map<String, String> map = getPropertyMap();
    if ((map != null) && (key != null)) {
      return map

举报

相关推荐

0 条评论