Springcloud学习笔记60---log4j2的MDC 原理及使用
1. 使用背景
slf4j是门面,log4j2是一种具体的实现。我们先看官网 slf4j的官网SLF4J 全称 Simple Logging Facade for Java 。主要是给java日志访问提供了一个标准,规范的API接口。具体实现由不同的日志框架实现,比如log4j2,logback。
我们项目中使用的log4j2日志框架,在日志输出的时候,我们有个需求,需要将我们全局唯一的流程流水id打印到log4j的日志文件中。
2. MDC 基础概念及原理
MDC ( Mapped Diagnostic Contexts ),顾名思义,其目的是为了便于我们诊断线上问题而出现的方法工具类。虽然,Slf4j 是用来适配其他的日志具体实现包的,但是针对 MDC功能,目前只有logback 以及 log4j 支持。
2.1 static 块的执行时机
类被加载了不一定就会执行静态代码块,只有一个类被主动使用的时候,静态代码才会被执行!
当一个类被主动使用时,Java虚拟就会对其初始化,如下六种情况为主动使用:
- 当创建某个类的新实例时(如通过new或者反射,克隆,反序列化等)
- 当调用某个类的静态方法时
- 当使用某个类或接口的静态字段时
- 当调用Java API中的某些反射方法时,比如类Class中的方法,或者java.lang.reflect中的类的方法时
- 当初始化某个子类时
- 当虚拟机启动某个被标明为启动类的类(即包含main方法的那个类)
2.2 MDC 源码解读
先来看看 MDC 对外提供的接口:
public class MDC { //Put a context value as identified by key //into the current thread's context map. public static void put(String key, String val); //Get the context identified by the key parameter. public static String get(String key); //Remove the context identified by the key parameter. public static void remove(String key); //Clear all entries in the MDC. public static void clear(); }
以put方法为入口;
public class MDC { static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA"; static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder"; static MDCAdapter mdcAdapter; /** * An adapter to remove the key when done. */ public static class MDCCloseable implements Closeable { private final String key; private MDCCloseable(String key) { this.key = key; } public void close() { MDC.remove(this.key); } } private MDC() { } /** * As of SLF4J version 1.7.14, StaticMDCBinder classes shipping in various bindings * come with a getSingleton() method. Previously only a public field called SINGLETON * was available. * * @return MDCAdapter * @throws NoClassDefFoundError in case no binding is available * @since 1.7.14 */ private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError { try { return StaticMDCBinder.getSingleton().getMDCA(); } catch (NoSuchMethodError nsme) { // binding is probably a version of SLF4J older than 1.7.14 return StaticMDCBinder.SINGLETON.getMDCA(); } }
//静态代码块,调用put方法时,先执行 static { try { mdcAdapter = bwCompatibleGetMDCAdapterFromBinder(); } catch (NoClassDefFoundError ncde) { mdcAdapter = new NOPMDCAdapter(); String msg = ncde.getMessage(); if (msg != null && msg.contains("StaticMDCBinder")) { Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\"."); Util.report("Defaulting to no-operation MDCAdapter implementation."); Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details."); } else { throw ncde; } } catch (Exception e) { // we should never get here Util.report("MDC binding unsuccessful.", e); } } /** * Put a diagnostic context value (the <code>val</code> parameter) as identified with the * <code>key</code> parameter into the current thread's diagnostic context map. The * <code>key</code> parameter cannot be null. The <code>val</code> parameter * can be null only if the underlying implementation supports it. * * <p> * This method delegates all work to the MDC of the underlying logging system. * * @param key non-null key * @param val value to put in the map * * @throws IllegalArgumentException * in case the "key" parameter is null */ 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); } }
public class Log4jMDCAdapter implements MDCAdapter { @Override public void put(final String key, final String val) { ThreadContext.put(key, val); } ..... }
最终进入CopyOnWriteSortedArrayThreadContextMap中;
class CopyOnWriteSortedArrayThreadContextMap implements ReadOnlyThreadContextMap, ObjectThreadContextMap, CopyOnWrite { ....... private final ThreadLocal<StringMap> localMap; //构造方法 public CopyOnWriteSortedArrayThreadContextMap() { this.localMap = createThreadLocalMap(); } // LOG4J2-479: by default, use a plain ThreadLocal, only use InheritableThreadLocal if configured. // (This method is package protected for JUnit tests.) private ThreadLocal<StringMap> createThreadLocalMap() { if (inheritableMap) { return new InheritableThreadLocal<StringMap>() { @Override protected StringMap childValue(final StringMap parentValue) { if (parentValue == null) { return null; } final StringMap stringMap = createStringMap(parentValue); stringMap.freeze(); return stringMap; } }; } // if not inheritable, return plain ThreadLocal with null as initial value return new ThreadLocal<>(); } ...... }
@Override public void put(final String key, final String value) { putValue(key, value); } @Override public void putValue(final String key, final Object value) { StringMap map = localMap.get(); map = map == null ? createStringMap() : createStringMap(map); map.putValue(key, value); map.freeze(); localMap.set(map); }
到此,我们可以看到MDC底层用的是ThreadLocal。
a)ThreadLocal 很多地方叫做线程本地变量,也有些地方叫做线程本地存储。
b)ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
c)ThreadLocal 使用场景为用来解决数据库连接、Session 管理等。
主要说明了两点:
MDC 主要用于保存上下文,区分不同的请求来源。
MDC 管理是按线程划分,并且子线程会自动继承母线程的上下文。
InheritableThreadLocal 说明:该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。通常,子线程的值与父线程的值是一致的;但是,通过重写这个类中的 childValue 方法,子线程的值可以作为父线程值的一个任意函数。
3 入门使用案例
package com.ttbank.flep.controller; import lombok.extern.slf4j.Slf4j; import org.slf4j.MDC; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; /** * <p> * 前端控制器 * </p> * * @author lucky * @since 2022-08-10 */ @RestController @RequestMapping("/user") @Slf4j public class UserController { public static final String REQ_ID = "REQ_ID"; @PostMapping("/mdctest") public void mdctest(){ MDC.put(REQ_ID, UUID.randomUUID().toString()); log.info("开始调用服务A,进行业务处理"); log.info("业务处理完毕,可以释放空间了,避免内存泄露"); MDC.remove(REQ_ID); log.info("REQ_ID 还有吗?{}", MDC.get(REQ_ID) != null); } }
代码编写完,貌似只有 MDC.put(K,V) 、MDC.remove(K) 两句是陌生的,先不着急解释它,等案例跑完就懂了,咱们继续往下看。
接下来配置 log4j2,通过 %X{REQ_ID} 来打印 REQ_ID 的信息,log4j2.xml 文件内容如下。
<!--控制台输出的相关配置--> <Console name="Console" target="SYSTEM_OUT" follow="true"> <PatternLayout pattern="%d{${LOG_DATEFORMAT_PATTERN}} | ${LOG_LEVEL_PATTERN} | %t | %c:%L | [%X{seq}] | [%X{REQ_ID}] - %m%n" /> </Console>
此时,查看日志信息;
2024-01-19 15:35:58.570 | INFO | http-nio-7012-exec-1 | com.ttbank.flep.controller.UserController:59 | [] | [1eb04a93-45d9-4b28-9c21-e0bdff136c47] - 开始调用服务A,进行业务处理
2024-01-19 15:35:58.570 | INFO | http-nio-7012-exec-1 | com.ttbank.flep.controller.UserController:60 | [] | [1eb04a93-45d9-4b28-9c21-e0bdff136c47] - 业务处理完毕,可以释放空间了,避免内存泄露
2024-01-19 15:35:58.570 | INFO | http-nio-7012-exec-1 | com.ttbank.flep.controller.UserController:62 | [] | [] - REQ_ID 还有吗?false
参考文献:
https://blog.csdn.net/f80407515/article/details/119239021
https://blog.csdn.net/m0_37556444/article/details/100142429
https://www.cnblogs.com/socoool/p/12742503.html