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♪(・ω・)ノ