> 技术文档 > 分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架

分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架


如果你目前只关注解决方案请点击这里直接跳转: SpringBoot2.x 使用TTL框架增强MDC实现TraceId传递

前置知识介绍

TTL(TransmittableThreadLocal)

TTL(TransmittableThreadLocal)是阿里巴巴开源的一个Java线程间传递数据的框架,它是对Java标准库中ThreadLocal的增强版本。

核心概念

ThreadLocal的局限性

标准的ThreadLocal存在一个主要问题:当使用线程池时,由于线程复用,ThreadLocal的值会从一个任务\"泄漏\"到下一个任务,导致数据污染。

TTL的解决方案

TTL框架通过\"可传递的ThreadLocal\"概念解决了这个问题,它能够在以下场景中正确传递线程本地变量:

  1. 线程池任务提交
  2. 异步调用
  3. 跨线程调用链

主要特性

  1. 线程池兼容:正确处理线程池场景下的ThreadLocal传递
  2. 透明使用:与标准ThreadLocal API兼容,易于迁移
  3. 框架集成:支持与各种异步框架(如RxJava、Hystrix等)集成
  4. 性能优化:相比其他方案(如InheritableThreadLocal)有更好的性能

使用场景

  1. 分布式跟踪系统中的调用链跟踪
  2. 多线程环境下的上下文传递
  3. 异步编程中的上下文保持
  4. 需要在线程池中传递上下文信息的任何场景

基本用法

1. 添加依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.12.6</version> </dependency>
2. 基本使用示例
// 创建TTL实例TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();// 设置值context.set(\"value-in-parent\");// 在子线程或线程池任务中获取值ExecutorService executor = Executors.newCachedThreadPool();executor.execute(() -> { System.out.println(context.get()); // 输出\"value-in-parent\"});// 清除context.remove();
3. 与线程池集成
ExecutorService executorService = Executors.newCachedThreadPool();// 使用TtlExecutors装饰线程池ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(executorService);TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();context.set(\"value-in-parent\");ttlExecutorService.execute(() -> { System.out.println(context.get()); // 正确获取父线程的值});

高级用法

1. 自定义拷贝策略
TransmittableThreadLocal<MyObject> ttl = new TransmittableThreadLocal<MyObject>() { @Override protected MyObject copy(MyObject parentValue) { return parentValue.clone(); // 自定义拷贝逻辑 }};
2. 与异步框架集成
// 例如与RxJava集成TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();context.set(\"value-in-parent\");Observable.create(...) .subscribeOn(Schedulers.io()) .subscribe(result -> { System.out.println(context.get()); // 正确获取上下文 });

实现原理

TTL的核心实现机制:
  1. 通过Java Agent或手动装饰Runnable/Callable来捕获任务提交时的ThreadLocal值
  2. 在任务执行前恢复捕获的ThreadLocal值
  3. 在任务执行后清理恢复的值

学习资源

  1. GitHub仓库 - 官方文档和源码
  2. 阿里巴巴中间件博客 - 有TTL的设计原理解析
  3. 官方示例代码 - 仓库中的example目录

注意事项

  1. 性能考虑:虽然TTL做了优化,但仍比普通ThreadLocal有额外开销
  2. 对象序列化:传递的对象需要是可序列化的(如果需要在分布式环境中传递)
  3. 内存泄漏:与ThreadLocal一样,需要及时remove避免内存泄漏

TTL是处理Java异步编程中上下文传递问题的优秀解决方案,特别适合在复杂的多线程和异步调用场景中使用。

MDC与TraceId详解:分布式系统日志追踪基础

1. MDC (Mapped Diagnostic Context) - 映射诊断上下文

什么是MDC?

MDC是日志框架(如Logback、Log4j)提供的一种机制,允许开发者在日志中存储上下文信息,这些信息会随着当前线程的日志输出自动附加。

MDC的核心特点:
  • 线程绑定:信息与当前线程绑定
  • 自动传递:在同一个线程内自动传递
  • 日志集成:自动添加到每一条日志中
MDC的基本使用(以Logback为例):
// 放入上下文信息MDC.put(\"userId\", \"user123\");MDC.put(\"requestId\", UUID.randomUUID().toString());// 日志输出时会自动包含这些信息logger.info(\"Processing payment\"); // 输出示例: [user123] [a1b2c3d4] Processing payment// 使用完成后清除MDC.clear();
MDC的典型应用场景:
  1. Web请求跟踪:记录用户ID、会话ID等
  2. 事务追踪:记录事务ID
  3. 系统间调用:保持调用链标识

2. TraceId - 跟踪标识符

什么是TraceId?

TraceId是分布式系统中用于唯一标识一个完整请求链路的标识符,贯穿于整个调用链的所有服务。

TraceId的特点:
  • 全局唯一:通常使用UUID或类似算法生成
  • 跨服务传递:通过HTTP头、RPC上下文等方式传递
  • 调用链聚合:用于将所有相关日志串联起来
TraceId的作用:
  1. 请求追踪:跟踪一个请求在分布式系统中的完整路径
  2. 问题排查:快速定位故障点
  3. 性能分析:分析请求在各服务的耗时情况

3. MDC与TraceId的关系

在实际应用中,TraceId通常会被放入MDC中,实现日志的自动标记:

// 在请求入口处生成traceIdString traceId = generateTraceId(); MDC.put(\"traceId\", traceId);// 所有日志都会自动带上traceIdlogger.info(\"Request started\");logger.debug(\"Processing step 1\");logger.error(\"Something went wrong\");// 输出示例:// [traceId:abc123] Request started// [traceId:abc123] Processing step 1// [traceId:abc123] Something went wrong

4. 分布式系统中的完整实现示例

4.1 服务入口(如Spring Boot Controller)
@RestControllerpublic class MyController { @GetMapping(\"/api\") public ResponseEntity<?> handleRequest(HttpServletRequest request) { // 从请求头获取或生成traceId String traceId = request.getHeader(\"X-Trace-Id\"); if(traceId == null) { traceId = UUID.randomUUID().toString(); } // 放入MDC MDC.put(\"traceId\", traceId); try { logger.info(\"Request received\"); // 处理请求... return ResponseEntity.ok(\"Success\"); } finally { MDC.clear(); // 确保清除 } }}
4.2 服务间调用(如Feign Client)
@FeignClient(name = \"other-service\")public interface OtherServiceClient { @GetMapping(\"/other\") String callOtherService();}// 配置Feign拦截器传递traceIdpublic class FeignTraceInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String traceId = MDC.get(\"traceId\"); if(traceId != null) { template.header(\"X-Trace-Id\", traceId); } }}
4.3 日志配置(logback.xml示例)
<appender name=\"STDOUT\" class=\"ch.qos.logback.core.ConsoleAppender\"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [traceId:%X{traceId}] %-5level %logger{36} - %msg%n</pattern> </encoder></appender>

5. 常见问题

Q: MDC在线程池中会丢失吗?
A: 是的,直接使用线程池会导致MDC丢失,需要使用TTL等解决方案。

Q: TraceId和SpanId有什么区别?
A: TraceId标识整个调用链,SpanId标识单个服务内的调用段,一个Trace包含多个Span。

Q: 生产环境应该用什么生成TraceId?
A: 通常使用UUID或基于时间戳+机器标识+序列号的算法(如Snowflake)。

希望这些信息能帮助你理解MDC和TraceId的基本概念!这是现代分布式系统开发中非常重要的基础知识。

为什么要使用TTL框架增强MDC实现TraceId传递

理解这个问题需要先明白MDC在并发环境中的局限性,以及TTL框架如何解决这些问题。下面我会从基础到深入逐步解释。

一、MDC在单线程中的工作方式

MDC(Mapped Diagnostic Context)本质上是基于ThreadLocal实现的:

// 伪代码表示MDC原理public class MDC { private static ThreadLocal<Map<String, String>> context = new ThreadLocal<>(); public static void put(String key, String val) { context.get().put(key, val); }}

在单线程中工作完美:

主线程:MDC.put(\"traceId\", \"123\") → 子任务执行 → 日志输出[traceId:123]

二、问题场景:线程池中的MDC丢失

当引入线程池时会出现问题:

// 线程池示例ExecutorService pool = Executors.newFixedThreadPool(2);MDC.put(\"traceId\", \"123\"); // 主线程设置pool.execute(() -> { // 子线程中MDC.get(\"traceId\") == null ❌ logger.info(\"Processing...\"); });
问题根源:
  1. 线程复用:线程池线程可能执行过其他任务
  2. ThreadLocal隔离:子线程无法自动获取父线程的ThreadLocal值
  3. 任务提交/执行分离:MDC值在任务提交时存在,但执行时可能已清除

三、TTL的解决方案原理

TTL(TransmittableThreadLocal)通过三种方式解决这个问题:

1. 值捕获(Capture)

在任务提交时(如execute()调用时):

// 伪代码public class TtlRunnable implements Runnable { private final Runnable runnable; private final Map<TransmittableThreadLocal<?>, ?> captured; TtlRunnable(Runnable runnable) { this.captured = TransmittableThreadLocal.capture(); // 捕获当前值 this.runnable = runnable; }}
2. 值回放(Replay)

在任务执行时:

public void run() { Map<TransmittableThreadLocal<?>, ?> backup = TransmittableThreadLocal.replay(captured); try { runnable.run(); // 执行实际任务 } finally { TransmittableThreadLocal.restore(backup); // 恢复原值 }}
3. 线程池装饰

通过TtlExecutors包装线程池:

ExecutorService ttlPool = TtlExecutors.getTtlExecutorService(pool);

四、关键优势对比

方案 线程池支持 异步链路支持 性能影响 代码侵入性 原生MDC ❌ ❌ 低 无 InheritableThreadLocal △ ❌ 中 中 TTL+MDC ✔️ ✔️ 低-中 低

五、生产环境最佳实践

  1. 初始化时机:在应用启动时尽早初始化TTL适配器

  2. TraceId生成

    public class TraceContext { private static final TransmittableThreadLocal<String> traceIdHolder = new TransmittableThreadLocal<>(); public static void startTrace() { traceIdHolder.set(\"trace-\" + UUID.randomUUID()); } public static String getTraceId() { return traceIdHolder.get(); }}
  3. 与异步框架集成

    // Reactor示例Hooks.onEachOperator(TracingSubscriber.asHook());// RxJava示例RxJavaPlugins.setScheduleHandler(TtlSchedulers::wrap);
  4. 清理策略

    try { // 业务代码} finally { MDC.clear(); TransmittableThreadLocal.clear();}

六、常见问题解决方案

问题1:TTL导致内存泄漏?

  • 解决方案:确保在finally块中清理TTL

问题2:与某些框架不兼容?

  • 解决方案:检查框架是否提供了线程装饰器接口(如Spring的TaskDecorator)

问题3:性能下降明显?

  • 解决方案:减少TTL中存储的数据量,避免存储大对象

通过TTL增强的MDC实现,可以确保traceId在复杂的异步编程场景中依然能正确传递,这对分布式系统的问题排查和调用链追踪至关重要。

SpringBoot2.x 使用TTL框架增强MDC实现TraceId传递

在介绍SpringBoot3.x 实现TTL框架增强MDC实现TraceId传递的解决方案时需要先介绍一下现在很多网上说的教程。网上说的教程都是针对于SpringBoot2.x版本的实现,如果你使用的是2.x就可以用这里的方案。如果不是这里你可以直接跳过,感兴趣也可以了解一下。

初始化示例项目

项目pom.xml文件如下
<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <groupId>org.tgr</groupId> <artifactId>ttl-log-demo</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>ttl-log-demo</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies></project>
实现原理

为了实现在线程池中可以传递traceId,我们需要做以下两个操作:

  1. 自定义MDCAdapter,使用TTL替换原先的Thread Local;
  2. 利用SpringBoot中的spi机制,加载MDCAdapter
自定义MDCAdapter

网上能搜出一大推,我这里给一份网上可用的代码
其实主要有以下关键的改动:

  1. 将ThreadLocal 变为 TransmittableThreadLocal
  2. 新增一个静态属性 TtlMDCAdapter mtcMDCAdapter以及一个静态方法
  3. 新增一个静态方法 getInstance()
    注意: 这里很多教程都要求你将自定义的MDCAdapter放在org.slf4j这个路径下。却不说为什么这么做。实际上是因为在静态代码块中使用到了MDC.mdcAdapter,MDC的mdcAdapter使用的是默认访问权限(default/package-private),只允许在同一个包下使用。因此我们的MDCAdapter必须与MDC在同一个包名下才可以通过 MDC.mdcAdapter = mtcMDCAdapter;直接赋值
package org.slf4j;import ch.qos.logback.classic.util.LogbackMDCAdapter;import com.alibaba.ttl.TransmittableThreadLocal;import org.slf4j.helpers.ThreadLocalMapOfStacks;import org.slf4j.spi.MDCAdapter;import java.util.*;/** * 重构{@link LogbackMDCAdapter}类,搭配TransmittableThreadLocal实现父子线程之间的数据传递 * */public class TtlMDCAdapter implements MDCAdapter { final ThreadLocal<Map<String, String>> readWriteThreadLocalMap = new TransmittableThreadLocal<>(); final ThreadLocal<Map<String, String>> readOnlyThreadLocalMap = new TransmittableThreadLocal<>(); private final ThreadLocalMapOfStacks threadLocalMapOfDeques = new ThreadLocalMapOfStacks(); private static TtlMDCAdapter mtcMDCAdapter; static { mtcMDCAdapter = new TtlMDCAdapter(); MDC.mdcAdapter = mtcMDCAdapter; } public static MDCAdapter getInstance() { return mtcMDCAdapter; } public void put(String key, String val) throws IllegalArgumentException { if (key == null) { throw new IllegalArgumentException(\"key cannot be null\"); } Map<String, String> current = readWriteThreadLocalMap.get(); if (current == null) { current = new HashMap<>(); readWriteThreadLocalMap.set(current); } current.put(key, val); nullifyReadOnlyThreadLocalMap(); } /** * Get the context identified by the key parameter. * 

*

* This method has no side effects. */ @Override public String get(String key) { Map<String, String> hashMap = readWriteThreadLocalMap.get(); if ((hashMap != null) && (key != null)) { return hashMap.get(key); } else { return null; } } /** *

Remove the context identified by the key parameter. *

*/ @Override public void remove(String key) { if (key == null) { return; } Map<String, String> current = readWriteThreadLocalMap.get(); if (current != null) { current.remove(key); nullifyReadOnlyThreadLocalMap(); } } private void nullifyReadOnlyThreadLocalMap() { readOnlyThreadLocalMap.set(null); } /** * Clear all entries in the MDC. */ @Override public void clear() { readWriteThreadLocalMap.set(null); nullifyReadOnlyThreadLocalMap(); } /** *

Get the current thread\'s MDC as a map. This method is intended to be used * internally.

* * The returned map is unmodifiable (since version 1.3.2/1.4.2). */
@SuppressWarnings(\"unchecked\") public Map<String, String> getPropertyMap() { Map<String, String> readOnlyMap = readOnlyThreadLocalMap.get(); if (readOnlyMap == null) { Map<String, String> current = readWriteThreadLocalMap.get(); if (current != null) { final Map<String, String> tempMap = new HashMap<>(current); readOnlyMap = Collections.unmodifiableMap(tempMap); readOnlyThreadLocalMap.set(readOnlyMap); } } return readOnlyMap; } /** * Return a copy of the current thread\'s context map. Returned value may be * null. */ public Map getCopyOfContextMap() { Map<String, String> readOnlyMap = getPropertyMap(); if (readOnlyMap == null) { return null; } else { return new HashMap<>(readOnlyMap); } } /** * Returns the keys in the MDC as a {@link Set}. The returned value can be * null. */ public Set<String> getKeys() { Map<String, String> readOnlyMap = getPropertyMap(); if (readOnlyMap != null) { return readOnlyMap.keySet(); } else { return null; } } @SuppressWarnings(\"unchecked\") public void setContextMap(Map contextMap) { if (contextMap != null) { readWriteThreadLocalMap.set(new HashMap<String, String>(contextMap)); } else { readWriteThreadLocalMap.set(null); } nullifyReadOnlyThreadLocalMap(); } @Override public void pushByKey(String key, String value) { threadLocalMapOfDeques.pushByKey(key, value); } @Override public String popByKey(String key) { return threadLocalMapOfDeques.popByKey(key); } @Override public Deque<String> getCopyOfDequeByKey(String key) { return threadLocalMapOfDeques.getCopyOfDequeByKey(key); } @Override public void clearDequeByKey(String key) { threadLocalMapOfDeques.clearDequeByKey(key); }}
利用SpringBoot中的spi机制,加载MDCAdapter
1. 自定义类实现 ApplicationContextInitializer接口

我找一个网上可以使用的代码贴在这里

package com.central.log.config;import org.slf4j.TtlMDCAdapter;import org.springframework.context.ApplicationContextInitializer;import org.springframework.context.ConfigurableApplicationContext;/** * 初始化TtlMDCAdapter实例,并替换MDC中的adapter对象 * */public class TtlMDCAdapterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { //加载TtlMDCAdapter实例 TtlMDCAdapter.getInstance(); }}

这里它使用了一个getInstance(),不知道是哪个天才想出来的方法。上面的TtlMDCAdapter的getInstance()只是获取mtcMDCAdapter属性而已,怎么和加载挂上钩了呢?怎么替换的MDCAdapter呢?
实际上它是通过调用这个静态方法,然后触发类加载,执行TtlMDCAdapter中的静态代码块,在静态代码块中new一个TtlMDCAdapter并把这个对象的值设置给了MDC.mdcAdapter

 static { mtcMDCAdapter = new TtlMDCAdapter(); MDC.mdcAdapter = mtcMDCAdapter; } public static MDCAdapter getInstance() { return mtcMDCAdapter; } 

实际上完成不需要这么麻烦,可以换一种方式实现同时也更直观,
TtlMDCAdapter改造后的代码如下

public class TtlMDCAdapter implements MDCAdapter { public static void initTtlMDCAdapter() { System.out.println(\"TtlMDCAdapter initTtlMDCAdapter\"); MDC.mdcAdapter = new TtlMDCAdapter(); } // 其他代码与原来保持一致即可 ...

TtlMDCAdapterInitializer 改造后的代码如下:

public class TtlMDCAdapterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @Override public void initialize(ConfigurableApplicationContext applicationContext) { TtlMDCAdapter.initTtlMDCAdapter(); }}
2. 配置spring.factories让springboot能够扫描到自定义的类

创建spring.factories文件,放在resources/META-INF/目录下
文件内容如下:

# Application Context Initializersorg.springframework.context.ApplicationContextInitializer=\\org.tgr.ttl.log.demo.initializer.TtlMDCAdapterInitializer

通过Springboot提供的SPI加载机制,让项目在启动的最开始就执行TtlMDCAdapter.initTtlMDCAdapter();从而将MDC中的mdcAdapter 替换成自定义的MdcAdapter ,这样后续的MDC相关操作都会使用自己增强好的MdcAdapter

项目完成结构如下

分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架

SpringBoot3.x 使用TTL框架增强MDC实现TraceId传递

前面介绍到的2.x使用的方法在SpringBoot3.x后其实无法生效。这里针对Springboot3.x再进行一次介绍

初始化示例项目

pom.xml文件如下
<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.12</version> </parent> <groupId>org.tgr</groupId> <artifactId>ttl-log-demo-17</artifactId> <version>1.0-SNAPSHOT</version> <packaging>jar</packaging> <name>ttl-log-demo-17</name> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies></project>
实现原理

为了实现在线程池中可以传递traceId,我们需要做以下两个操作:

  1. 自定义MDCAdapter,使用TTL替换原先的Thread Local;
  2. 自定义LogbackServiceProvider替换默认的LogbackServiceProvider
自定义MDCAdapter
package org.slf4j;import ch.qos.logback.classic.util.LogbackMDCAdapter;import com.alibaba.ttl.TransmittableThreadLocal;import org.slf4j.helpers.ThreadLocalMapOfStacks;import java.util.*;/** * 重构{@link LogbackMDCAdapter}类,搭配TransmittableThreadLocal实现父子线程之间的数据传递 * */public class TtlMDCAdapter extends LogbackMDCAdapter { // BEWARE: Keys or values placed in a ThreadLocal should not be of a type/class // not included in the JDK. See also https://jira.qos.ch/browse/LOGBACK-450 final ThreadLocal<Map<String, String>> readWriteThreadLocalMap = new TransmittableThreadLocal<>(); final ThreadLocal<Map<String, String>> readOnlyThreadLocalMap = new TransmittableThreadLocal<>(); private final ThreadLocalMapOfStacks threadLocalMapOfDeques = new ThreadLocalMapOfStacks(); /** * Put a context value (the val parameter) as identified with the * key parameter into the current thread\'s context map. Note that * contrary to log4j, the val parameter can be null. * 

*

* If the current thread does not have a context map it is created as a side * effect of this call. *

*

* Each time a value is added, a new instance of the map is created. This is * to be certain that the serialization process will operate on the updated * map and not send a reference to the old map, thus not allowing the remote * logback component to see the latest changes. * * @throws IllegalArgumentException in case the \"key\" parameter is null */ public void put(String key, String val) throws IllegalArgumentException { if (key == null) { throw new IllegalArgumentException(\"key cannot be null\"); } Map<String, String> current = readWriteThreadLocalMap.get(); if (current == null) { current = new HashMap<String, String>(); readWriteThreadLocalMap.set(current); } current.put(key, val); nullifyReadOnlyThreadLocalMap(); } /** * Get the context identified by the key parameter. *

*

* This method has no side effects. */ @Override public String get(String key) { Map<String, String> hashMap = readWriteThreadLocalMap.get(); if ((hashMap != null) && (key != null)) { return hashMap.get(key); } else { return null; } } /** *

Remove the context identified by the key parameter. *

*/ @Override public void remove(String key) { if (key == null) { return; } Map<String, String> current = readWriteThreadLocalMap.get(); if (current != null) { current.remove(key); nullifyReadOnlyThreadLocalMap(); } } private void nullifyReadOnlyThreadLocalMap() { readOnlyThreadLocalMap.set(null); } /** * Clear all entries in the MDC. */ @Override public void clear() { readWriteThreadLocalMap.remove(); nullifyReadOnlyThreadLocalMap(); } /** *

Get the current thread\'s MDC as a map. This method is intended to be used * internally.

* * The returned map is unmodifiable (since version 1.3.2/1.4.2). */
@SuppressWarnings(\"all\") public Map<String, String> getPropertyMap() { Map<String, String> readOnlyMap = readOnlyThreadLocalMap.get(); if (readOnlyMap == null) { Map<String, String> current = readWriteThreadLocalMap.get(); if (current != null) { final Map<String, String> tempMap = new HashMap<>(current); readOnlyMap = Collections.unmodifiableMap(tempMap); readOnlyThreadLocalMap.set(readOnlyMap); } } return readOnlyMap; } /** * Return a copy of the current thread\'s context map. Returned value may be * null. */ public Map getCopyOfContextMap() { Map<String, String> readOnlyMap = getPropertyMap(); if (readOnlyMap == null) { return null; } else { return new HashMap<>(readOnlyMap); } } /** * Returns the keys in the MDC as a {@link Set}. The returned value can be * null. */ public Set<String> getKeys() { Map<String, String> readOnlyMap = getPropertyMap(); if (readOnlyMap != null) { return readOnlyMap.keySet(); } else { return null; } } @SuppressWarnings(\"unchecked\") public void setContextMap(Map contextMap) { if (contextMap != null) { readWriteThreadLocalMap.set(new HashMap<String, String>(contextMap)); } else { readWriteThreadLocalMap.set(null); } nullifyReadOnlyThreadLocalMap(); } @Override public void pushByKey(String key, String value) { threadLocalMapOfDeques.pushByKey(key, value); } @Override public String popByKey(String key) { return threadLocalMapOfDeques.popByKey(key); } @Override public Deque<String> getCopyOfDequeByKey(String key) { return threadLocalMapOfDeques.getCopyOfDequeByKey(key); } @Override public void clearDequeByKey(String key) { threadLocalMapOfDeques.clearDequeByKey(key); }}
自定义LogbackServiceProvider
package org.slf4j;import ch.qos.logback.classic.LoggerContext;import ch.qos.logback.classic.spi.LogbackServiceProvider;import ch.qos.logback.classic.util.ContextInitializer;import ch.qos.logback.core.CoreConstants;import ch.qos.logback.core.joran.spi.JoranException;import ch.qos.logback.core.status.StatusUtil;import ch.qos.logback.core.util.StatusPrinter;import org.slf4j.helpers.BasicMarkerFactory;import org.slf4j.helpers.Util;import org.slf4j.spi.MDCAdapter;import org.slf4j.spi.SLF4JServiceProvider;/** * 重写 LogbackServiceProvider */public class TgrLogbackServiceProvider extends LogbackServiceProvider { final static String NULL_CS_URL = CoreConstants.CODES_URL + \"#null_CS\"; /** * Declare the version of the SLF4J API this implementation is compiled against. * The value of this field is modified with each major release. */ // to avoid constant folding by the compiler, this field must *not* be final public static String REQUESTED_API_VERSION = \"2.0.99\"; // !final private LoggerContext defaultLoggerContext; private IMarkerFactory markerFactory; private TtlMDCAdapter mdcAdapter; @Override public void initialize() { System.out.println(\"TgrLogbackServiceProvider initialize...\"); defaultLoggerContext = new LoggerContext(); defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME); initializeLoggerContext(); defaultLoggerContext.start(); markerFactory = new BasicMarkerFactory(); mdcAdapter = new TtlMDCAdapter(); // set the MDCAdapter for the defaultLoggerContext immediately defaultLoggerContext.setMDCAdapter(mdcAdapter); } private void initializeLoggerContext() { try { try { new ContextInitializer(defaultLoggerContext).autoConfig(); } catch (JoranException je) { Util.report(\"Failed to auto configure default logger context\", je); } // LOGBACK-292 if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) { StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext); } // contextSelectorBinder.init(defaultLoggerContext, KEY); } catch (Exception t) { // see LOGBACK-1159 Util.report(\"Failed to instantiate [\" + LoggerContext.class.getName() + \"]\", t); } } @Override public ILoggerFactory getLoggerFactory() { return defaultLoggerContext; } @Override public IMarkerFactory getMarkerFactory() { return markerFactory; } @Override public MDCAdapter getMDCAdapter() { return mdcAdapter; } @Override public String getRequestedApiVersion() { return REQUESTED_API_VERSION; }}
使用SpringBoot的Spi机制 加载TgrLogbackServiceProvider

resources/META-INF/services/目录下新建org.slf4j.spi.SLF4JServiceProvider文件。内容如下:

# 这里的值应该是你自定义的LogbackServiceProvider类的全路径限定名org.slf4j.TgrLogbackServiceProvider

项目启动后日志打印会出现一些警告

SLF4J: Class path contains multiple SLF4J providers.
SLF4J: Found provider [org.slf4j.TgrLogbackServiceProvider@2a3046da]
SLF4J: Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@2a098129]
SLF4J: See https://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual provider is of type [org.slf4j.TgrLogbackServiceProvider@2a3046da]

这样就表示加载成功,需要注意的是你自定义的LogbackServiceProvider必须是第一个加载的,这样才会生效。否则将不会生效
这里的警告意思就是扫描到了两个SLF4J providers,最终使用的providers 是 [org.slf4j.TgrLogbackServiceProvider@2a3046da]
你也可以通过java命令行参数的方式强制指定要使用的providers

java -jar -Dslf4j.provider=org.slf4j.TgrLogbackServiceProvider ...

这种方式同样可以实现,日志会打印出如下内容:

SLF4J: Attempting to load provider “org.slf4j.TgrLogbackServiceProvider” specified via “slf4j.provider” system property

表示使用命令行参数强制指定Provider的实现为org.slf4j.TgrLogbackServiceProvider
到这里就结束了。为什么springboot2.x 和 springboot3.x的实现方式完全不一样呢?这里需要牵扯到Springboot中默认日志框架logback各个版本的实现差异。下面一章通过源码的方式介绍二者之间的差异

原因

Spring Boot 2.x

我使用的是2.7.14. 它自动引入的logback依赖是 1.2.12
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
为了搞明白为什么同样的代码在SpringBoot2.x就能生效但是到了SpringBoot3.x却没有用,我们要先了解他们日志框架是怎么将我们日志中的%X{traceId} 这种占位符替换成具体的值的呢。

我们在SpringBoot2.x的应用中通过改变MDC.mdcAdapter的属性来实现日志打印的增强,那么这个占位符替换具体的值就肯定是和我们的MDCAdapter有关系了。我们可以看到MDCAdapter接口有哪些方法
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
很明显,put是设置一个键值对,那将占位符替换成具体的值肯定涉及到一个get操作,所以要么是get方法 要么就是getCopyOfContextMap方法了。get明显就是获取一个key 的值 getCopyOfContexntMap的方法应该就是获取Map,Map包含所有设置进去的所有键值对了。
通过Debug,实际上是调用了getCopyOfContexntMap()方法
因为springboot采用的日志框架是logback,因为我们找到Logback的实现 LogbackMDCAdapter

 public Map<String, String> getCopyOfContextMap() { Map<String, String> hashMap = (Map)this.copyOnThreadLocal.get(); return hashMap == null ? null : new HashMap(hashMap); }

这个方法实际上就是将存在当前线程的threadlocal中的map拿出来而已。
那很显然,如果我们想让我们的MDCAdapter生效,就得让MDCAdapter的具体实现变成我们自己的MDCAdapter。
通过堆栈,我们再往前看是谁调用了MDCAdapter的getCopyOfContextMap(),找一下是在哪里选择具体的实现类的。
往上找我们会找到一个LoggingEvent的类下的getMDCPropertyMap()方法。
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架

我们在这里就能很清楚的看到 它使用了MDC.getMDCAdapter()方法获取MDCAdapter 的具体实现类。
我们再看这个方法里的实现
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
它实际上返回了MDC的mdcAdapter属性,到这里就很明显了。我们增强的时候不是刚好设置了这个属性吗
回顾网上的代码

 static { mtcMDCAdapter = new TtlMDCAdapter(); MDC.mdcAdapter = mtcMDCAdapter; } public static MDCAdapter getInstance() { return mtcMDCAdapter; }

其实就是设置了 MDC.mdcAdapter 的值,然后保证这个方法能在项目的最开始去执行就可以了。那这就是SpringBoot2.x 中自定义MDCAdapter的实现原理。

Spring Boot 3.x

我们来到SpringBoot3.x的版本再看,首先可以看到logback的版本已经发生了变化 版本是1.4.11
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架

同样的流程,我这回就直接到LoggingEvent的类下的getMDCPropertyMap()方法。看看新版本的实现是什么
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
我们可以看到,新版的getMDCPropertyMap()方法已经更新了,不再使用MDC的mtcAdapter属性了。而是使用了一个loggerContext的对象中的getMDCAdapter()方法。
我们点进去看看这个方法的实现是什么
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
还是一样使用了自己的mdcAdapter属性。那我们下一步就是找这个属性是怎么赋值
经过 我的一系列排查,发现这个属性只有一个setMDCAdapter()方法,在这个类中并没有找到有设置MDCAdapter的地方,构造方法中也没有。
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架那我们再往上找一层,看看是哪里初始化这个loggerContext的,在初始化的地方应该会有设置的方法
在这里我跳过排查的过程,直接给大家展示调用链
首先在SpringApplication类中有一个Log类型的属性logger
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
它通过LogFactorygetLog方法获取Log对象。在LogFactory这个抽象类中getLog方法实际用到了LogAdapter的静态方法createLog创建日志对象
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
LogAdapter.createLog()方法中使用到了一个属性createLog,它是一个static final的函数式接口,并在静态代码块中设置
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
在这个静态代码块中,通过debug最终发现createLog的值是Slf4jAdapter::createLocationAwareLog;这个方法。顺着这个方法点进去。它的内部实现是通过LoggerFactory.getLogger()方法拿到Logger对象,并最终将其转成Log返回。
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
我们再点进LoggerFactory.getLogger()方法的实现
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
这个方法的实现实际上是通过getILoggerFactory()获取一个ILoggerFactory实例.那这个ILoggerFactory是什么呢 ,它实际上是一个接口,它的定义如下:
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
我们在这里发现了之前找到的那个LoggerContext类的身影,这个接口只有一个getLogger的方法。我们先不管这个getLogger的实现,先看看SpringBoot是怎么确认使用LoggerContext作为ILoggerFactory的实现类的
回到LoggerFactory.getLogger()这个方法的定义这里,点进去看看这个getLogger方法实际上做了什么
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
方法的实现很简单,调用了getProvider()获取到一个对象并且调用了获取到的对象的getLoggerFactory方法
在看看getProvider方法的实现
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
通过debug,SpringBoot实际上会到SUCCESSFUL_INITIALIZATION这个分支,具体为什么会走这个分支,感兴趣的可以自己了解一下,我这里就没有深入了,大概应该是引入了日志框架后,加载了一些SLF4JServiceProvider接口实现类就会到这里
这个分支实际是返回了一个静态属性PROVIDER,SpringBoot通过调用PROVIDER getILoggerFactory()最终确定了ILoggerFactory的实现类

我们继续跟进,看看这个静态属性又是在哪里赋值的。最终找到了bind方法
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
bind方法调用了findServiceProviders()查询到了所有SLF4JServiceProvider类实例列表,我们点进去看看这个SLF4JServiceProvider是什么东西,记性好的就能联想到,我们之前实现Springboot3.x增强MDCAdapter的时候就是自定义了一个类并实现了这个SLF4JSerivceProvider
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
通过这个接口的定义我们就能看到,它有一个getLoggerFactory()的方法以及一个看着和MDC增强密切相关的方法getMDCAdapter()方法。可以看到我们自定义的实现排在第一位,而在第二位的就是LogbackServiceProvider,实际上这个就是SpringBoot的日志框架logback的默认实现了。
已经知道了SLF4JServiceProvider是什么后,我们再回到bind方法
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
可以看到它首先查询了全部的SLF4JServiceProvider的实现类列表,最后把列表的第一个元素赋值给了静态属性PROVIDER,PROVIDER再调用它的initialize完成了初始化。那这就是为什么我们之前自定义SLF4JServiceProvider实现类提到的要保证我们自己的类是第一个加载的原因了。因为在框架中它只会取第一个实现类,为了保证PROVIDER的类型是我们自己的,就得保证我们自己类的加载顺序是在第一位.

接下来我们再看看findServiceProviders()这个方法是怎么查询到项目中SLF4JServiceProvider的实现类的。
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
它的实现又涉及到了另外两个方法loadExplicitlySpecifiedgetServiceLoader.,这两个方法才是实际去加载类的执行流程.我就带大家看看第一个方法吧,第二个大家可以自行研究,第二个大致就是通过SPI机制加载类。
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
在这里方法的实现中,我们就发现了它实际读取了系统属性slf4j.provider,很明显,我们的那个虚拟机参数

-Dslf4j.provider=org.slf4j.TgrLogbackServiceProvider

就是从这里得到的。
到这里我们就知道了如何让LoggerFactoryPROVIDER变成我们自己的实现类TgrLogbackServiceProvider
再回到bind方法
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
看看initialize()这个方法。这里我展示我们自定义的 和SpringBoot默认的Logback实现类的initialize()方法实现。

TgrLogbackServiceProvider
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架

LogbackServiceProvider分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
可以看到就是在这个方法中设置了MdcAdapter,那我们就是在这个方法中将原来的LogbackMDCAdapter替换成了我们自己的TtlMDCAdapter
同时我们也终于看到了那个LoggerContext.很显然,关键的代码就是在initialize()这里,这里给LoggerContext进行了实例化,并调用了setMDCAdapter()方法设置LoggerContext中的MdcAdapter,然后日志在打印时再调用LoggerContext中的MdcAdapter属性获取用于替换占位符的属性map。

最后我们只需要确认这个LoggerContext是怎么返回的就可以了。还记得LoggerContext是一个ILoggerFactory接口吗,以及LoggerFactorygetILoggerFactory()的实现
分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
回到这里,我们此时就知道了getProvider()最终会拿到我们自己的TgrLogbackServiceProvider,如果我们没有自定义的话,那这里就是拿到SpringBoot的默认实现LogbackServiceProvider,我们看看两个实现类中这个方法是怎么实现的

TgrLogbackServiceProvider分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
LogbackServiceProvider分布式微服务架构日志处理解决方案: SpringBoot使用Ttl框架增强MDC,实现traceId在子线程的传递_微服务日志框架
可以看到两个实现都是单纯的返回了自己的defaultLoggerContext属性,而这个属性就是在initialize()方法中进行初始化的。只不过我们自定义的defaultLoggerContext它调用的setMDCAdapter()传入的是我们自己定义的TtlMDCAdapter.我们就是通过这样实现了MDCAdapter的增强

自此整个源码分析就结束了,如果我哪里说的不清楚或者有错误的希望大家都够在评论区指出,我在这感激不尽.