自定义MDCAdapter,进行线程池级别的日志ID传递
文章目录
- 前言
- 正文
- 一、实现原理
- 二、项目环境
- 三、项目代码
- 3.1 pom.xml
- 3.2 TtlMDCAdapter
- 3.3 ThreadLocalMapOfStacks
- 3.4 StaticMDCBinder
- 3.5 启动类& 线程池配置
- 3.6 logback-spring.xml
- 四、测试
- 4.1 TestController
- 4.2 调拥接口,观察日志
前言
有这样一个场景:
在一个springboot项目中,使用了 logback进行日志输出。但是项目中有部分功能使用了线程池,使用MDC进行日志ID的输出。而默认的
LogbackMDCAdapter
无法传递参数到线程池中。因为它本身的实现是依赖于ThreadLocal
进行数据传递的。
本文就将解决这个参数传递的问题,使用阿里巴巴开源的TransmittableThreadLocal
来重新实现一个MDCAdapter
。
PS:有了解过 ThreadLocal 原理 以及 TTL 的朋友肯定知道,TTL 可以进行线程池级别的参数传递。这里不做过多描述。
正文
一、实现原理
为了实现在线程池中,可以传递 logid ,我们需要做两个操作:
- 自定义
MDCAdapter
,使用 TTL 替换原先的ThreadLocal
; - 在自己项目中,创建 “同包同名” 的
org.slf4j.impl.StaticMDCBinder
,并指定MDCAdapter
是自己自定义的实现;
二、项目环境
- Java版本:Java 1.8
- SpringBoot版本: 2.7.18
- TTL 版本:2.14.2
三、项目代码
3.1 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>
<groupId>org.example</groupId>
<artifactId>pine-mdc-ttl</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>pine-mdc-ttl</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>1.8</java.version>
<java.compiler.source>${java.version}</java.compiler.source>
<java.compiler.target>${java.version}</java.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.18</spring-boot.version>
<ttl.version>2.14.2</ttl.version>
<lombok.version>1.18.34</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
</dependency>
<!--日志颜色-->
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<mainClass>org.BootDemoApplication</mainClass>
<skip>true</skip>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>${ttl.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>
</project>
3.2 TtlMDCAdapter
package org.pine.mdcttl;
import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.spi.MDCAdapter;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义MDC适配器:与TransmittableThreadLocal结合使用
*/
public class TtlMDCAdapter implements MDCAdapter {
private final ThreadLocalMapOfStacks threadLocalMapOfDeques = new ThreadLocalMapOfStacks();
private final ThreadLocal<Map<String, String>> context =
new TransmittableThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> initialValue() {
return new HashMap<>();
}
@Override
public Map<String, String> copy(Map<String, String> parentValue) {
return parentValue != null ? new HashMap<>(parentValue) : null;
}
};
@Override
public void put(String key, String val) {
context.get().put(key, val);
}
@Override
public String get(String key) {
return context.get().get(key);
}
@Override
public void remove(String key) {
context.get().remove(key);
}
@Override
public void clear() {
context.get().clear();
context.remove();
}
@Override
public Map<String, String> getCopyOfContextMap() {
return new HashMap<>(context.get());
}
@Override
public void setContextMap(Map<String, String> contextMap) {
context.set(new HashMap<>(contextMap));
}
public void pushByKey(String key, String value) {
this.threadLocalMapOfDeques.pushByKey(key, value);
}
public String popByKey(String key) {
return this.threadLocalMapOfDeques.popByKey(key);
}
public Deque<String> getCopyOfDequeByKey(String key) {
return this.threadLocalMapOfDeques.getCopyOfDequeByKey(key);
}
public void clearDequeByKey(String key) {
this.threadLocalMapOfDeques.clearDequeByKey(key);
}
}
3.3 ThreadLocalMapOfStacks
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.pine.mdcttl;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
public class ThreadLocalMapOfStacks {
final ThreadLocal<Map<String, Deque<String>>> tlMapOfStacks = new TransmittableThreadLocal<>();
public ThreadLocalMapOfStacks() {
}
public void pushByKey(String key, String value) {
if (key != null) {
Map<String, Deque<String>> map = (Map)this.tlMapOfStacks.get();
if (map == null) {
map = new HashMap();
this.tlMapOfStacks.set(map);
}
Deque<String> deque = (Deque)((Map)map).get(key);
if (deque == null) {
deque = new ArrayDeque();
}
((Deque)deque).push(value);
((Map)map).put(key, deque);
}
}
public String popByKey(String key) {
if (key == null) {
return null;
} else {
Map<String, Deque<String>> map = (Map)this.tlMapOfStacks.get();
if (map == null) {
return null;
} else {
Deque<String> deque = (Deque)map.get(key);
return deque == null ? null : (String)deque.pop();
}
}
}
public Deque<String> getCopyOfDequeByKey(String key) {
if (key == null) {
return null;
} else {
Map<String, Deque<String>> map = (Map)this.tlMapOfStacks.get();
if (map == null) {
return null;
} else {
Deque<String> deque = (Deque)map.get(key);
return deque == null ? null : new ArrayDeque(deque);
}
}
}
public void clearDequeByKey(String key) {
if (key != null) {
Map<String, Deque<String>> map = (Map)this.tlMapOfStacks.get();
if (map != null) {
Deque<String> deque = (Deque)map.get(key);
if (deque != null) {
deque.clear();
}
}
}
}
}
3.4 StaticMDCBinder
package org.slf4j.impl;
import org.pine.mdcttl.TtlMDCAdapter;
import org.slf4j.spi.MDCAdapter;
/**
* 自定义MDC绑定器实现:
* 1. 绑定自定义的ttl-MDC适配器
* 2. 覆盖原先的org.slf4j.impl.StaticMDCBinder,使自定义的ttl-mdc适配器生效
*/
public class StaticMDCBinder {
public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();
public MDCAdapter getMDCA() {
return new TtlMDCAdapter();
}
public String getMDCAdapterClassStr() {
return TtlMDCAdapter.class.getName();
}
}
3.5 启动类& 线程池配置
package org.pine;
import com.alibaba.ttl.threadpool.TtlExecutors;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ExecutorService;
@SpringBootApplication
public class BootDemoApplication {
public static void main(String[] args) {
SpringApplication.run(BootDemoApplication.class, args);
}
@Bean(name = "ttlExecutorService")
public ExecutorService ttlExecutorService() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(100);
executor.initialize();
// 关键步骤:用TTL包装线程池
return TtlExecutors.getTtlExecutorService(executor.getThreadPoolExecutor());
}
}
3.6 logback-spring.xml
<?xml version="1.0" encoding="UTF-8" ?>
<configuration debug="false">
<!-- 启用ANSI颜色支持(需要Jansi库) -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 带颜色的日志格式 -->
<!-- 格式化输出: %d表示日期, %thread表示线程名, %-5level: 级别从左显示5个字符宽度 %msg:日志消息, %n是换行符 %logger{0}:表示类名不进行任何缩写,输出完整的类名,%logger{50}:表示类名的最大长度为50个字符,超过的部分会被截断。-->
<!-- %clr(...){color}是Spring Boot对Logback的扩展,用于输出带颜色的日志。 颜色名称支持:black, red, green, yellow, blue, magenta, cyan, white等-->
<pattern>[%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){green}] %clr([%thread]){faint} %clr([logid=%X{logid:-system}]){red} %highlight(%-5level) %cyan(%logger{50}) - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志输出级别 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
四、测试
4.1 TestController
package org.pine.testcontroller;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
@RestController
@Slf4j
@RequestMapping("/test")
public class TestController {
@Resource(name = "ttlExecutorService")
private ExecutorService executorService;
@RequestMapping("/test1")
public String test() {
String logId = UUID.randomUUID().toString();
MDC.put("logid",logId );
log.info("logId:{}", logId);
executorService.submit(() -> {
log.info("executorService-logId:{}", MDC.get("logid"));
});
return "test";
}
}
4.2 调拥接口,观察日志
访问:http://localhost:8080/test/test1
观察到的日志结果:
[2025-03-14 14:54:10.304] [http-nio-8080-exec-1] [logid=a5610ff9-f250-4c3f-81f5-15c36d1a6278] INFO org.pine.testcontroller.TestController - logId:a5610ff9-f250-4c3f-81f5-15c36d1a6278
[2025-03-14 14:54:10.317] [ThreadPoolTaskExecutor-1] [logid=a5610ff9-f250-4c3f-81f5-15c36d1a6278] INFO org.pine.testcontroller.TestController - executorService-logId:a5610ff9-f250-4c3f-81f5-15c36d1a6278
可以看到线程池中输出的结果中,拼接了与其主线程相同的logid。这也是本文需要实现的效果。