欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-11-09 21:41:10
Java中使用的日志的实现框架有很多种,常用的log4j和logback以及java.util.logging,而log4j是apache实现的一个开源日志组件(Wrapped implementations),logback是slf4j的原生实现(Native implementations)。需... ......

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

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