太原网站建设质量推荐零售客户电商网站
文章目录
- 前言
- 正文
- 一、实现原理
- 二、项目环境
- 三、项目代码
- 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>>() {@Overrideprotected Map<String, String> initialValue() {return new HashMap<>();}@Overridepublic Map<String, String> copy(Map<String, String> parentValue) {return parentValue != null ? new HashMap<>(parentValue) : null;}};@Overridepublic void put(String key, String val) {context.get().put(key, val);}@Overridepublic String get(String key) {return context.get().get(key);}@Overridepublic void remove(String key) {context.get().remove(key);}@Overridepublic void clear() {context.get().clear();context.remove();}@Overridepublic Map<String, String> getCopyOfContextMap() {return new HashMap<>(context.get());}@Overridepublic 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。这也是本文需要实现的效果。