Java日志Log4j或者Logback的NDC和MDC功能


NDC和MDC的區別

Java中使用的日志的實現框架有很多種,常用的log4j和logback以及java.util.logging,而log4j是apache實現的一個開源日志組件(Wrapped implementations),logback是slf4j的原生實現(Native implementations)。需要說明的slf4j是Java簡單日志的門面(The Simple Logging Facade for Java),如果使用slf4j日志門面,必須要用到slf4j-api,而logback是直接實現的,所以不需要其他額外的轉換以及轉換帶來的消耗,而slf4j要調用log4j的實現,就需要一個適配層,將log4j的實現適配到slf4j-api可調用的模式。

說完基本的日志框架的區別之后,我們再看看NDC和MDC。

不管是log4j還是logback,打印的日志要能體現出問題的所在,能夠快速的定位到問題的症結,就必須攜帶上下文信息(context information),那么其存儲該信息的兩個重要的類就是NDC(Nested Diagnostic Context)和MDC(Mapped Diagnositc Context)。

NDC采用棧的機制存儲上下文,線程獨立的,子線程會從父線程拷貝上下文。其調用方法如下:

1.開始調用
NDC.push(message);

2.刪除棧頂消息
NDC.pop();

3.清除全部的消息,必須在線程退出前顯示的調用,否則會導致內存溢出。
NDC.remove();

4.輸出模板,注意是小寫的[%x]
log4j.appender.stdout.layout.ConversionPattern=[%d{yyyy-MM-dd HH:mm:ssS}] [%x] : %m%n

MDC采用Map的方式存儲上下文,線程獨立的,子線程會從父線程拷貝上下文。其調用方法如下:

1.保存信息到上下文
MDC.put(key, value);

2.從上下文獲取設置的信息
MDC.get(key);

3.清楚上下文中指定的key的信息
MDC.remove(key);

4.清除所有
clear()

5.輸出模板,注意是大寫[%X{key}]
log4j.appender.consoleAppender.layout.ConversionPattern = %-4r [%t] %5p %c %x - %m - %X{key}%n

最后需要注意的是:

  • Use %X Map中全部數據
  • Use %X{key} 指定輸出Map中的key的值
  • Use %x 輸出Stack中的全部內容

MDC的使用例子

//MdcUtils.java
// import ...MdcConstants // 這個就是定義一個常量的類,定義了SERVER、SESSION_ID等
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

public class MdcUtils {

    private final static Logger logger = LoggerFactory.getLogger(MdcUtils.class);

    private static void put(String key, Object value) {
        if (value != null) {
            String val = value.toString();
            if (StringUtils.isNoneBlank(key, val)) {
                MDC.put(key, val);
            }
        }
    }

    public static String getServer() {
        return MDC.get(MdcConstants.SERVER);
    }

    public static void putServer(String server) {
        put(MdcConstants.SERVER, server);
    }

    public static String getSessionId() {
        return MDC.get(MdcConstants.SESSION_ID);
    }

    public static void putSessionId(String sId) {
        put(MdcConstants.SESSION_ID, sId);
    }

    public static void clear() {
        MDC.clear();
        logger.debug("mdc clear done.");
    }
}

上述工具類中MdcConstants是定義一個常量的類,定義了SERVER、SESSION_ID等,put方法就是調用了slf4j的MDC的put方法。其他方法類比。

看看使用該工具類的具體方式:

// MdcClearInterceptor.java
import ...MdcUtils; // 導入上面的工具類
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class MdcClearInterceptor extends HandlerInterceptorAdapter {
    @Override
    public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler)
                    throws Exception {
        MdcUtils.clear();
    }
}

在該攔截器中,重寫了afterConcurrentHandlingStarted方法,該方法執行了工具類的clear方法,也就是通過調用slf4j的clear方法清除了本次會話上下文的日志信息。為什么要放在afterConcurrentHandlingStarted方法中呢?這恐怕得從springmvc的攔截器的實現說起。

springmvc的攔截HandlerInterceptor接口定義了三個方法(代碼如下),具體說明在方法注釋上:

public interface HandlerInterceptor {  
    //在控制器方法調用前執行
    //返回值為是否中斷,true,表示繼續執行(下一個攔截器或處理器)
    //false則會中斷后續的所有操作,所以我們需要使用response來響應請求
    boolean preHandle(  
            HttpServletRequest request, HttpServletResponse response,   
            Object handler)   
            throws Exception;  

    //在控制器方法調用后,解析視圖前調用,我們可以對視圖和模型做進一步渲染或修改
    void postHandle(  
            HttpServletRequest request, HttpServletResponse response,   
            Object handler, ModelAndView modelAndView)   
            throws Exception;  
    //整個請求完成,即視圖渲染結束后調用,這個時候可以做些資源清理工作,或日志記錄等
    void afterCompletion(  
            HttpServletRequest request, HttpServletResponse response,   
            Object handler, Exception ex)  
            throws Exception;  
}  

很多時候,我們只需要上面這3個方法就夠了,因為我們只需要繼承HandlerInterceptorAdapter就可以了,HandlerInterceptorAdapter間接實現了HandlerInterceptor接口,並為HandlerInterceptor的三個方法做了空實現,因而更方便我們定制化自己的實現。

相對於HandlerInterceptor,HandlerInterceptorAdapter多了一個實現方法afterConcurrentHandlingStarted(),它來自HandlerInterceptorAdapter的直接實現類AsyncHandlerInterceptor,AsyncHandlerInterceptor接口直接繼承了HandlerInterceptor,並新添了afterConcurrentHandlingStarted()方法用於處理異步請求,當Controller中有異步請求方法的時候會觸發該方法時,異步請求先支持preHandle、然后執行afterConcurrentHandlingStarted。異步線程完成之后執行preHandle、postHandle、afterCompletion。

那至於這些可能用到的日志字段從什么地方賦值呢,也就是什么地方調用MDCUtils.put()方法呢?一般我們都會實現一個RequestHandlerInterceptor,在preHandler方法中處理日志字段即可。如下:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
        throws Exception {
    if (DispatcherType.ASYNC.equals(request.getDispatcherType())) {
        return true;
    }
    
    // 開始保存信息到日志上下文
    MdcUtils.putServer(request.getServerName());
    String sId = request.getHeader(HeaderConstants.SESSION_ID);
    MdcUtils.putSessionId(sId);

    if (sessionWhiteList.contains(request.getPathInfo())) {
        return true;
    }

    // TODO 處理其他業務
}

還沒完,就目前看,我們已經有兩個自定義的攔截器實現了。怎么使用,才能將日志根據我們的意願正確的打印呢?必然,攔截器是有順序的,如果配置了多個攔截器,會形成一條攔截器鏈,執行順序類似於AOP,前置攔截先定義的先執行,后置攔截和完結攔截(afterCompletion)后注冊的后執行。

Soga,我們需要清除上次請求的一些無用的信息,再次將我們的信息寫入到MDC中(攔截器的配置在DispatcherServlet中),由於afterConcurrentHandlingStarted()方法需要異步請求觸發,因此我們需要在web.xml的DispatchServlet配置增加<async-supported>true</async-supported>配置。

<mvc:interceptors>
    <bean class="com.xxx.handler.MdcClearInterceptor"/>
    <bean class="com.xxx.handler.RequestContextInterceptor"/>
</mvc:interceptors>

或者這樣:

<mvc:interceptors>
    <!-- 前置攔截器 -->
    <mvc:interceptor>
        <!-- 這里面還以增加一些攔截條件-->
        <!--<mvc:exclude-mapping path="/user/logout"/>-->
        <!-- 用戶退出登錄請求 -->
        <!-- <mvc:exclude-mapping path="/home/"/> -->
        <!--在home中定義了無須登錄的方法請求,直接過濾攔截-->
        <!-- <mvc:mapping path="/**"/>-->
        <bean class="com.xxx.handler.MdcClearInterceptor"/>
    </mvc:interceptor>

    <!-- 后置攔截器 -->
    <mvc:interceptor>
        <bean class="com.xxx.handler.RequestContextInterceptor"/>
    </mvc:interceptor>
</mvc:interceptors>


該文首發《虛懷若谷》個人博客,轉載前請務必署名,轉載請標明出處。

古之善為道者,微妙玄通,深不可識。夫唯不可識,故強為之容:
豫兮若冬涉川,猶兮若畏四鄰,儼兮其若客,渙兮若冰之釋,敦兮其若朴,曠兮其若谷,混兮其若濁。
孰能濁以靜之徐清?孰能安以動之徐生?
保此道不欲盈。夫唯不盈,故能敝而新成。

請關注我的微信公眾號:下雨就像彈鋼琴,Thanks♪(・ω・)ノ

微信二維碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM