当前位置: 首页 > news >正文

自定义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 ,我们需要做两个操作:

  1. 自定义MDCAdapter,使用 TTL 替换原先的 ThreadLocal
  2. 在自己项目中,创建 “同包同名” 的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。这也是本文需要实现的效果。

相关文章:

  • 安装 Powerlevel10k 及 Oh My Zsh 的使用
  • opencascade 源码学习几何变换 BRepBuilderAPI-BRepBuilderAPI_Transform
  • 【GB28181】RTSP服务器传输AAC音频
  • JVM垃圾收集器相关面试题(1)
  • WPS 接入 DeepSeek-R1 深度实践:打造全能AI办公助手
  • VXLAN 组播 RP
  • QT编程之QGIS
  • 【Flutter】数据库实体类构造函数加密注意事项
  • 深度学习有哪些算法?
  • 30、map 和 unordered_map的区别和实现机制【高频】
  • FreeRTOS之信号量
  • 【后端】【django】Django DRF `@action` 详解:自定义 ViewSet 方法
  • 微信小程序实现根据不同的用户角色显示不同的tabbar并且可以完整的切换tabbar
  • 母婴商城系统Springboot设计与实现
  • 冠珠瓷砖×郭培“惟质致美”品质主题片上映,讲述高定艺术背后的致美品质故事
  • SSM基础专项复习5——Maven私服搭建(2)
  • 1.2、Java中的私有方法
  • 前端笔试高频算法题及JavaScript实现
  • 安科瑞EMS3.0开启企业微电网能源管理新篇章
  • 了解printf函数
  • 减重人生|吃得越少越好?比体重秤上的数字,更有意义的是什么?
  • 韦尔股份拟更名豪威集团:更全面体现公司产业布局,准确反映未来战略发展方向
  • 海南乐城管理局原局长贾宁已赴省政协工作,曾从河南跨省任职
  • 博物馆书单|走进博物馆,去体验一场与文明的对话
  • 解锁儿时愿望!潘展乐战胜孙杨,全国冠军赛男子400自夺冠
  • AI赋能科学红毯,机器人与科学家在虚实之间叩问“科学精神”