跟我学Springboot开发后端管理系统8:AOP+logback+MDC日志输出

2020/06/07

MDC介绍

在比较复杂的应用中,一个请求需要走很多个方法的处理,怎么样才能快速查找一个请求的全部日志呢。在分布式系统中,我们可以用链路追踪,比如zipkin、skywalking去快速查找日志,从而定位问题。在比较复杂的单体管理系统中,我们可以使用slf4j的MDC去实现类似的功能。

MDC ( Mapped Diagnostic Contexts ),是为了便于我们诊断线上问题而出现的方法工具类。使用ThreadLocal实现的,在MDC中的变量,每个线程都会有单独的副本,多线程不会相互干扰。MDC功能,logback 和 log4j 提供了支持。在Matrix-Web中,使用logback和slf4j进行日志的答应。

MDC原理

MDC类是一个静态工具类,对外提供了类似Map的接口:

public class MDC { 
   // 清空 map 所有的条目。
   public static void clear(); 
   // 根据 key 值返回相应的对象
   public static object get(String key); 
   // 返回所有的 key 值 . 
   public static Enumeration getKeys(); 
   // 把 key 值和关联的对象,插入 map 中
   public static void put(String key, Object val), 
   // 删除 key 对应的对象
   public static  remove(String key) 
}

为了弄清楚MDC的原理,我们来跟下MDC的源码,比如put方法,最终交给mdcAdapter去处理。

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

跟踪代码mdcAdapter是由StaticMDCBinder初始化出现的,即LogbackMDCAdapter的实例。

public class StaticMDCBinder {

    /**
     * The unique instance of this class.
     */
    public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();

    private StaticMDCBinder() {
    }

    /**
     * Currently this method always returns an instance of 
     * {@link StaticMDCBinder}.
     */
    public MDCAdapter getMDCA() {
        return new LogbackMDCAdapter();
    }

LogbackMDCAdapter中put方法最终是由copyOnThreadLocal去处理的。

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

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

        if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
            Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
            newMap.put(key, val);
        } else {
            oldMap.put(key, val);
        }
    }

而copyOnThreadLocal是一个ThreadLocal。

final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();

由此可见MDC最终是由ThreadLocal去存放和取key、value的。

在Matrix-Web中使用MDC

在Matrix-web中使用Filter去做MDC的处理,在请求进入业务请求逻辑之前,将前端生成的REQUEST_ID存储在MDC中。当请求的业务逻辑完成后,将MDC清除。

public class LogFilter implements Filter {
  
    public static final String REQUEST_ID="REQUEST_ID";
  
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
      
          //REQUEST_ID由前端生成,
           MDC.put(REQUEST_ID, getRequestId(httpServletRequest));
         
            
           filterChain.doFilter(servletRequest, servletResponse);
      
          
           MDC.clear();
        }

    }
}

在logback.xml中配置打印REQUEST_ID,用这个配置%X{REQUEST_ID}。具体配置如下:

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{REQUEST_ID} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 按照每天生成日志文件 -->
    <appender name="FILE"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/${APP_NAME}.log.%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--日志文件保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %X{REQUEST_ID} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

定义一个RespDTO,该类用于Controller统一返回结果,在该类中,会自动requestId赋值给这个类,统一给前端,这样前端页面也能够从请求结果上查到requestId。

public class RespDTO<T> implements Serializable {

    public int code = 0;
    public String message = "";
    public T data;
    public String requestId;

    public static RespDTO onSuc(Object data) {
        RespDTO resp = new RespDTO();
        String requestId = MDC.get(REQUEST_ID);
        if (!StringUtils.isEmpty(requestId)) {
            resp.requestId = requestId;
        }
        resp.message="sucess";
        resp.data = data;
        return resp;
    }

    @Override
    public String toString() {
        return "RespDTO{" +
                "code=" + code +
                ", error='" + message + '\'' +
                ", data=" + data +
                '}';
    }
}

使用MDC能够将一个请求的所有业务处理逻辑的日志通过一个唯一的标识串起来,方便日志的排查。

本文为原创文章,转载请标明出处。
本文链接:https://www.fangzhipeng.com/springboot/2020/06/07/mdc.html
本文出自方志朋的博客


(转载本站文章请注明作者和出处 方志朋-forezp

宝剑锋从磨砺出,梅花香自苦寒来,用心分享,一起成长,做有温度的攻城狮!

Post Directory