1. 程式人生 > 其它 >slf4j MDC是個好東西

slf4j MDC是個好東西

技術標籤:技術整理經驗之談java多執行緒面試經驗分享

slf4j MDC是個好東西

簡介

MDC 全拼 Mapped Diagnostic Contexts,是SLF4J類日誌系統中實現分散式多執行緒日誌資料傳遞的重要工具。

同時,使用者也可利用MDC將一些執行時的上下文資料打印出來。


什麼意思呢?

常規情況下,寫打日誌的程式碼時,一般都是log.info、log.warn、log.error將想要打的日誌進行拼裝和格式化,打到日誌輸出中。MDC能幹什麼呢?能在不改動log.xxx打日誌程式碼的情況下,在最終的日誌輸出的指定位置列印額外的資訊。而這,就是靠MDC進行傳遞實現的。


應用場景

在日誌中自動列印框架/元件方面的資訊

例如:

  • 全鏈路日誌traceId
  • 使用者請求的IP地址、user-agent

程式碼示例

一般配合AOP / Filter / Interceptor使用

@Around(value = "execution(* com.xx.xx.facade.impl.*.*(..))", argNames="pjp")
public Object validator(ProceedingJoinPoint pjp) throws Throwable {
  try {
    String traceId = TraceUtils.
begin(); MDC.put("mdc_trace_id", traceId); Object obj = pjp.proceed(args); return obj; } catch(Throwable e) { //TODO 處理錯誤 } finally { TraceUtils.endTrace(); } }

程式碼通過AOP記錄了每次請求的traceId,並使用變數"mdc_trace_id"記錄到MDC內。

在日誌配置檔案裡需要設定變數才能將"mdc_trace_id"輸出到日誌檔案中。以logback配置檔案為例,看日誌第10行%X{mdc_trace_id}:

<appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
      <file>${CATALINA_BASE}/logs/all.log</file>
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
          <!-- daily rollover -->
          <fileNamePattern>${CATALINA_BASE}/logs/all.%d{yyyy-MM-dd}.log</fileNamePattern>
          <!-- keep 30 days' worth of history -->
          <maxHistory>30</maxHistory>
      </rollingPolicy>
      <encoder charset="UTF-8">
          <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - traceId:[%X{mdc_trace_id}] - %msg%n</pattern>
      </encoder>
  </appender>

可見的優勢

1、如果你的系統早已上線,突然有一天老闆說我們增加一些使用者資料到日誌裡分析一下。如果沒有MDC,你不得不在N個工程裡翻天覆地的“傳引數+改打日誌的程式碼”,你肯定很崩潰,不懂技術的老闆也很無奈(就多加幾點資訊,這麼大動靜嗎?)。而MDC能讓你很從容的完成此事。

  • 筆者團隊就有這樣的情況,但提出在日誌里加內容的是我們自已的優化想法:將pinpoint的鏈路標識打到應用日誌裡去。

2、使程式碼簡潔、日誌風格統一、變更靈活。


對MDC原始碼的窺探

MDC所在的jar包

在這裡插入圖片描述

此處以 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中的具體實現

既然一般都是結合AOP使用MDC,那麼還是要考慮內部方法實現時的支撐情況,例如:多執行緒


下面看一下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);
            }
        }
    }
}
  • MDC只有一種用法:MDC.put(X,Y)。那麼,在MDC的具體實現包中,肯定會有個Map作為儲存容器。如上,LogbackMDCAdapter中也有Map<String, String>。
  • MDC內的key-value要能在呼叫鏈路中都能列印,那麼Map肯定是儲存在ThreadLocal中傳遞。
  • Map<String, String>儲存在InheritableThreadLocal中,即AOP內真正的業務方法內部若進行了子執行緒的建立,MDC內的key-value也能正常的列印到日誌中。但,內部若是執行緒池的方式執行細分業務,則執行緒池任務內列印的日誌則不會有此內容(執行緒池的ThreadLocal傳遞需要用TransmittableThreadLocal)。比較遺憾,logback沒有預留這一點的SPI。