企业级线程池 traceId 闭环:上下文传递与全链路追踪实战

定位:本文深入剖析 traceId 在线程池场景下的跨线程传递机制,涵盖问题背景、核心组件源码解析、三段式传递设计、父子上下文链路、日志关联查询与故障排查。
摘要 :Java 并发编程中,ThreadLocal 在线程池复用场景下天然断层,traceId 丢失、串号、日志无法关联是长期困扰开发者的痛点。本文从源码级拆解一套企业级 "捕获 → 恢复 → 清理"三段式透传方案 :通过装饰器闭包机制,在不侵入业务代码的前提下,实现 traceId 在父子、孙层级线程间无感流转。全文深度解析 Context 父子链设计、ContextTaskDecorator 核心实现、MDCTemplate 日志关联技巧 及 SkyWalking 分布式链路无缝集成,并给出完整的 12 文件最小移植清单 与 traceId 丢失/串号/嵌套断裂三级故障排查手册。一文贯通原理、实现、集成、移植、排障全链路,让异步调用追踪有据可查,整套方案可直接落地至任意 Java 项目。

线程池架构设计与实践请参阅 企业级 Java 线程池架构全解:从设计理念到生产实践与项目移植

目录

  1. 问题背景与设计目标
  2. 源码文件清单与职责说明
  3. 核心组件源码解析
  4. 传递链路全景
  5. 传递机制详解
  6. 日志关联查询
  7. [SkyWalking 集成](#SkyWalking 集成)
  8. 传递注意事项
  9. [trace 组件移植指南](#trace 组件移植指南)
  10. 故障排查

1. 问题背景与设计目标

1.1 问题背景

在 Java 并发编程中,ThreadLocal 是实现线程隔离上下文传递的标准方案。然而在线程池场景 下,ThreadLocal 存在一个致命问题:

线程池中的线程是复用的 。工作线程一旦创建后会被反复使用执行不同任务,ThreadLocal

不会自动从提交任务的线程(父线程)传递到执行任务的工作线程(子线程)。

这导致以下后果:

  1. traceId 丢失:主线程生成的 traceId 无法传递到子线程,子线程日志中 traceId 为空
  2. 日志无法关联:无法通过 traceId 将主线程与子线程的日志串联起来,排查问题困难
  3. 上下文串号风险:线程复用时如果不清理,前一个任务的上下文可能泄漏到后一个任务

1.2 设计目标

目标 实现方式
traceId 自动透传 装饰器模式在任务提交时自动捕获/恢复 traceId,业务代码无感知
父子链路保留 不是简单复制 traceId,而是建立 Context 父子关系链,支持多层级调用链追踪
无泄漏 finally 块强制清理 ThreadLocal,防止线程复用导致的上下文泄漏
异常兜底 装饰器中的异常不影响业务执行,catch 后继续
SkyWalking 集成 可选集成 SkyWalking APM,实现分布式链路追踪

1.3 解决思路

复制代码
ThreadLocal 不能跨线程传递
        ↓
但对象的引用可以通过闭包跨线程
        ↓
在父线程先把上下文保存为普通变量(捕获)
        ↓
让 lambda 表达式捕获这个变量(闭包携带)
        ↓
在子线程中恢复写入子线程的 ThreadLocal(恢复)
        ↓
执行完毕后清理 ThreadLocal(清理)

这就是 "捕获 → 恢复 → 清理"三段式传递机制 的核心思想。


2. 源码文件清单与职责说明

2.1 trace/juc 包(com.xxx.trace.juc)--- 装饰器与线程池集成

文件 行数 职责 核心类/接口
DecoratorThreadPoolExecutor.java 64 装饰器线程池,继承 ThreadPoolExecutor,在 execute() 中调用 TaskDecorator.decorate() 包装任务后再委托父类执行 DecoratorThreadPoolExecutor extends ThreadPoolExecutor
ContextTaskDecorator.java 97 traceId 上下文透传装饰器,三段式传递的核心实现 ContextTaskDecorator implements TaskDecorator
TaskDecorator.java 11 线程任务装饰器接口,定义 decorate(Runnable) 方法 TaskDecorator(接口)
SkyWalkingUtil.java 48 SkyWalking 链路追踪工具,创建/继承/销毁 Span SkyWalkingUtil

2.2 trace 包(com.xxx.trace)--- 上下文管理与 MDC 操作

文件 行数 职责
Context.java 44 trace 上下文模型,持有 traceId、parent(父上下文)、preTraceId、snapshot(SkyWalking 快照)、自定义数据 Map
ContextHolder.java 43 ThreadLocal 全局上下文持有器,提供 getOrBuildContext() / setContext() / remove() 方法
MDCTemplate.java 101 MDC 操作模板,生成/设置 traceId 到 SLF4J MDC,记录 parentTraceId / preTraceId 日志用于跨线程关联查询
MdcInfo.java 21 MDC 信息模型,包含 TraceType 数组和是否刷新 traceId 的标志
TraceConfig.java 17 跟踪配置,控制是否禁用 SkyWalking
TraceContextWrap.java 17 基于 SkyWalking 的父子线程上下文粘合包装器
MDC.java (注解) 24 方法级注解,声明 TraceType(LOG/TIMER/METRICS/EVENT)和是否刷新 traceId
MDCAspect.java 42 @MDC 注解的 AOP 切面,拦截标注方法并调用 MDCTemplate.traceMethod()
TraceType.java 7 追踪类型枚举:LOG(日志)、TIMER(计时)、METRICS(指标)、EVENT(事件)

2.3 工具类

文件 职责
NanoIdUtils.java traceId 生成工具,使用 NanoId 算法生成短且唯一的 traceId

3. 核心组件源码解析

3.1 Context --- trace 上下文模型

源码

java 复制代码
/**
 * trace 上下文
 *
 * @author liteng
 */
@Data
public class Context {

    // 自定义数据
    private Map<String, Object> data = new HashMap<>();
    
    // 方法开始时间
    private Long startTime;

    //当前线程的 traceId
    private String traceId;

    //父线程的上下文(关键!)
    private Context parent;

    //SkyWalking 快照
    private ContextSnapshot snapshot;

    //前序调用链路的 traceId
    private String preTraceId;

    public String getParentTraceId() {
        if (parent == null) {
            return null;
        }
        return parent.traceId;
    }

    public Object get(String key) {
        return data.get(key);
    }

    public void put(String key, Object value) {
        data.put(key, value);
    }
}

设计要点

  • parent 字段是链式结构Context 持有一个 parent 引用指向父线程的 Context,形成链表而非简单复制值
  • getParentTraceId() :通过 parent.traceId 获取父线程的 traceId
  • 支持多层级调用链context.getParent().getParentTraceId() 可以获取祖父线程的 traceId
  • preTraceId:记录 MDC 中原有的 traceId(前序调用链路),用于方法嵌套场景

3.2 ContextHolder --- ThreadLocal 全局上下文持有器

源码

java 复制代码
public class ContextHolder {
    private static final ThreadLocal<Context> contextThreadLocalHolder = new ThreadLocal<>();

    public static Context getContext() {
        return contextThreadLocalHolder.get();
    }

    public static Context getOrBuildContext() {
        Context context = getContext();
        if (context != null) {
            return context;
        }
        context = buildAndAddContext();
        return context;
    }

    public static Context buildAndAddContext() {
        Context context = new Context();
        setContext(context);
        return context;
    }

    public static void setContext(Context input) {
        contextThreadLocalHolder.set(input);
    }

    public static void remove() {
        contextThreadLocalHolder.remove();
    }
}

设计要点

  • getOrBuildContext():获取或创建上下文,避免空指针
  • remove():清理 ThreadLocal,这是防止线程复用泄漏的关键
  • 线程池场景下,每个工作线程的 ThreadLocal 是独立的,需要显式设置和清理

3.3 ContextTaskDecorator --- traceId 透传装饰器(核心组件)

这是整个 traceId 跨线程传递机制的核心组件

源码

java 复制代码
/**
 * traceId 上下文透传装饰器
 * 设计目的:
 * 1. 在任务提交时 捕获父线程上下文,在子线程执行时恢复,保证跨线程的traceId透传且不发生串号
 * 2. 通过装饰器模式 + ThreadLocal复制 解决线程池场景下,ThreadLocal不会自动传递给子线程的问题
 */
@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ContextTaskDecorator implements TaskDecorator {

    /**
     * 线程池前缀 标识装饰器归属那个线程池
     */
    private String operateName;

    /**
     * 链路追踪配置
     */
    private TraceConfig traceConfig;

    /**
     * 在主线程(父线程)中对任务进行装饰,将父线程的上下文(traceId)放到lambda表达式(未执行)
     * 将装饰好的lambda表达式丢到线程池队列,lambda表达式在子线程中执行
     * 解决核心问题:
     * ThreadLocal不能跨线程传递,但对象的引用可以通过闭包跨线程
     * 所以必须在父线程先把上下文保存为普通变量,再让lambda捕获这个变量,最后在子线程中恢复写入子线程的ThreadLocal形成三段式传递:
     * 线程上下文传递流程图
     *@formatter:off
    主线程 ThreadLocal → 局部变量 parentCxt(捕获)
                            ↓ lambda 闭包携带
    子线程执行 → before(parentCxt) → 写入子线程 ThreadLocal(恢复)
                                            ↓ finally
                                        ContextHolder.remove()(清理)
     * @formatter:on
     */
    @Override
    public Runnable decorate(Runnable runnable) {
        // 捕获当前线程(父线程)的上下文
        Context parentCxt = ContextHolder.getOrBuildContext();
        // 在父线程捕获 SkyWalking 快照,供子线程链路继承
        if (isSkyWalkingEnabled()) {
            SkyWalkingUtil.addContextSnapshot(parentCxt);
        }
        //返回包装后的Runnable(此lanbda表达式在子线程执行)
        return () -> {
            try {
                //建立父子线程上下文关系链
                before(parentCxt);
                //执行业务逻辑
                runnable.run();
            } finally {
                try {
                	// 先结束子线程 LocalSpan,再清理 ThreadLocal,避免链路残留
                    if (isSkyWalkingEnabled()) {
                        SkyWalkingUtil.endSpan();
                    }
                    // 清理操作,避免线程池中的线程复用时出现上下文信息泄漏,造成traceId串号
                    ContextHolder.remove();
                } catch (Exception e) {
                    log.error("finally, enhance fail ", e);
                }
            }
        };
    }

    /**
     * 在任务执行之前,将父线程的上下文设置到执行任务的子线程中
     * 设计意图:不是简单地复制traceId,而是建立父子关系链,保留完整的调用层级
     * 子线程可通过childCxt.getParentTraceId()获取父线程的traceId,实现多层级调用链路追踪
     *
     * @param parentCxt {@link Context}
     */
    private void before(Context parentCxt) {
        try {

            // 在任务执行之前,子线程构建自己的上下文
            Context childCxt = ContextHolder.getOrBuildContext();
            if (parentCxt != null) {
                //建立父子上下文关系链
                childCxt.setParent(parentCxt);
            }
            // 子线程恢复父线程 SkyWalking 快照并创建 LocalSpan,实现跨线程链路继承
            if (isSkyWalkingEnabled()) {
                SkyWalkingUtil.continueSpan(childCxt, buildSpanName(), parentCxt);
            }
        } catch (Exception e) {
            log.error("before, enhance fail ", e);
        }
    }
    /**
     * SkyWalking 集成是否开启(由 TraceConfig.disableSkyWalking 控制,默认开启)
     */
    private boolean isSkyWalkingEnabled() {
        return traceConfig == null || !traceConfig.isDisableSkyWalking();
    }

    /**
     * 构建子线程 Span 名称:{线程池前缀}-execute,无前缀时使用默认名称
     */
    private String buildSpanName() {
        if (operateName == null || operateName.trim().isEmpty()) {
            return "thread-pool-execute";
        }
        return operateName + "-execute";
    }
}
三段式传递流程
复制代码
主线程 ThreadLocal → 局部变量 parentCxt(捕获)
                        ↓ lambda 闭包携带
子线程执行 → before(parentCxt) → 写入子线程 ThreadLocal(恢复)
                                ↓ finally
                            ContextHolder.remove()(清理)

逐行解析

步骤 代码位置 执行线程 作用
① 捕获 Context parentCxt = ContextHolder.getOrBuildContext() 父线程(提交线程) 获取父线程的 Context 对象,保存为局部变量
② 恢复 before(parentCxt)childCxt.setParent(parentCxt) 子线程(工作线程) 在子线程创建/获取 Context,设置 parent 引用
③ 执行 runnable.run() 子线程 执行原始业务逻辑,可通过 ContextHolder 获取上下文
④ 清理 finally { ContextHolder.remove() } 子线程 清理 ThreadLocal,防止线程复用时上下文泄漏
关键设计决策
  1. 为什么在父线程捕获,而不是子线程?

    decorate() 是在 DecoratorThreadPoolExecutor.execute() 中被调用的,而 execute() 是在提交任务的线程(父线程)

    中执行的。此时父线程的 ThreadLocal 中有 traceId。如果不在此处捕获,等任务到了子线程的 ThreadLocal 中就没有 traceId 了。

  2. 为什么用闭包而不是直接传值?

    Java 的 lambda 表达式会捕获外部局部变量的引用。parentCxt 作为一个普通的 Java 对象引用,通过闭包从父线程"携带"

    到子线程。这绕过了 ThreadLocal 不能跨线程传递的限制。

  3. 为什么建立父子链而不是简单复制 traceId?

    如果只是复制 traceId,就无法区分"哪个线程派生了哪个线程"。建立 parent 引用链后,子线程可以通过 getParentTraceId()

    获取父线程 traceId,支持多层级调用链追踪。

  4. 为什么 finally 中要 try-catch?

    如果清理操作本身抛出异常,不能影响已经执行完毕的业务逻辑。catch 后记录日志即可。

3.4 TaskDecorator --- 装饰器接口

源码

java 复制代码
public interface TaskDecorator {
    Runnable decorate(Runnable runnable);
}

设计意图 :定义装饰器接口而非直接绑定 ContextTaskDecorator,允许未来扩展其他类型的装饰器(如安全上下文传递、租户上下文传递等)。

3.5 DecoratorThreadPoolExecutor --- 装饰器线程池

源码

java 复制代码
/**
 * 装饰器线程池
 * 采用装饰器模式在任务被线程池执行前,插入一段装饰逻辑TaskDecorator(如:traceId透传、MDC上下文传播)
 * 让每个提交到线程池的任务自动 "携带" 父线程的上下文信息
 */
@Setter
public class DecoratorThreadPoolExecutor extends ThreadPoolExecutor {

    // 任务装饰器,装饰器的注入点,接收原始任务,返回包装后任务,若不设置,走普通线程池逻辑
    private TaskDecorator taskDecorator;

    /**
     * 所有线程池参数直接委托给父类ThreadPoolExecutor
     * 自身只负责接收并保存 taskDecorator
     */
    public DecoratorThreadPoolExecutor(int corePoolSize,
                                       int maximumPoolSize,
                                       long keepAliveTime,
                                       TimeUnit unit,
                                       BlockingQueue<Runnable> workQueue,
                                       ThreadFactory threadFactory,
                                       RejectedExecutionHandler reject,
                                       TaskDecorator decorator) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, reject);
        taskDecorator = decorator;
    }

    /**
     * 线程执行
     * 关键点:
     * decorate()是在提交任务的线程(父线程)中执行的,而不是在线程池的工程线程中,
     * 这意味着装饰器可以在此处捕获父线程的traceId/MDC上下文,然后返回一个包装任务,该包装任务在工作线程执行时恢复这些上下文
     * @formatter:on
     */
    @Override
    public void execute(@NonNull Runnable command) {
        if (taskDecorator != null) {
            // 使用 taskDecorator对子线程进行装饰,获取包装后的Runnable
            Runnable decorate = taskDecorator.decorate(command);
            //线程池执行Runnable,子线程从队列取出lambda并执行
            super.execute(decorate);
        } else {
            super.execute(command);
        }
    }
}

执行流程

复制代码
业务线程提交任务(command)
  ↓
execute() 被调用
  ↓
taskDecorator 存在?
  ├── 是 → command 被 decorate() 包装 → 包装后的 Runnable 传给 super.execute()
  └── 否 → command 直接传给 super.execute()(降级为普通线程池)

关键点decorate() 是在提交任务的线程(父线程)中执行的,而不是在线程池的工作线程中。这意味着装饰器可以在此处捕获父线程的

traceId/MDC 上下文,然后返回一个包装任务,该包装任务在工作线程执行时恢复这些上下文。

3.6 MDCTemplate --- MDC 操作模板

源码

java 复制代码
public class MDCTemplate {
    private static final Logger log = LoggerFactory.getLogger("MDC");
    private static final String MDC_STR = "MDC";

    public static <V> V traceMethod(Callable<V> callable, String signatureName, MdcInfo mdcInfo) {
        Context context = null;
        try {
            // 1. 从 MDC 获取 preTraceId(前序调用链路的 traceId)
            String preTraceId = MDC.get(MDC_STR);
            // 2. 从 ContextHolder 获取/创建 Context
            context = ContextHolder.getOrBuildContext();
            if (preTraceId != null) {
                context.setPreTraceId(preTraceId);
            }
            // 3. 获取 parentTraceId(来自 ContextTaskDecorator 设置的 parent)
            String parentTraceId = context.getParentTraceId();
            // 4. 生成 traceId(NanoId),设置到 Context + MDC
            String traceId = getOrGeneratorTraceId(mdcInfo, context);
            context.setTraceId(traceId);
            context.setStartTime(System.currentTimeMillis());
            MDC.put(MDC_STR, traceId);
            // 5. 记录关联日志
            if (parentTraceId != null) {
            	//线程池内外关联
                log.info("{} start,parentTraceId=[{}] ", signatureName, parentTraceId);
            }
            if (preTraceId != null) {
            	//线程池内外关联
                log.info("{} start,preTraceId=[{}] ", signatureName, preTraceId);
            }
            // 6. 执行目标方法
            return callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 7. 清理 MDC上下文
            MDC.remove(MDC_STR);
            if (null != context) {
                if (Arrays.asList(mdcInfo.getTypes()).contains(TraceType.TIMER)) {
                    long endTime = System.currentTimeMillis();
                    log.info("{},Exiting, spend time:{} ", signatureName,
                            (endTime - context.getStartTime()));
                }
                // 恢复 preTraceId 到 MDC(支持方法嵌套)
                if (StringUtils.isNotBlank(context.getPreTraceId())) {
                    MDC.put(MDC_STR, context.getPreTraceId());
                    context.setPreTraceId(null);
                }
            }
        }
    }

    private static String getOrGeneratorTraceId(MdcInfo mdcInfo, Context context) {
        String traceId;
        if (!mdcInfo.isRefresh()) {
            // 不刷新模式:如果 Context 中已有 traceId 则复用
            if (Objects.isNull(context.getTraceId())) {
                traceId = NanoIdUtils.randomNanoId();
            } else {
                traceId = context.getTraceId();
            }
        } else {
            // 刷新模式:总是生成新的 traceId
            traceId = NanoIdUtils.randomNanoId();
        }
        return traceId;
    }
}

MDCTemplate 在传递链中的角色

  • 主线程入口@MDC 注解触发 MDCAspectMDCTemplate.traceMethod() → 生成 traceId 设置到 MDC + ContextHolder
  • 子线程入口 :如果子线程的方法也标注了 @MDC,会调用 MDCTemplate.traceMethod(),此时 context.getParentTraceId()
    已经有值(由 ContextTaskDecorator.before() 设置),记录 parentTraceId 关联日志

3.7 MDCAspect --- AOP 切面

源码

java 复制代码
@Aspect
@Component
public class MDCAspect {

    private static final Logger log = LoggerFactory.getLogger("MDC");

    @Around("@annotation(MDC)")
    public Object traceMethod(ProceedingJoinPoint joinPoint, MDC MDC) throws Throwable {
        String signatureName = null;
        try {
            Class<?> targetClass = AopUtils.getTargetClass(joinPoint.getTarget());
            String className = targetClass.getSimpleName();
            String methodName = joinPoint.getSignature().getName();
            signatureName = className + "." + methodName;
        } catch (Exception e) {
            log.error("traceMethod error", e);
            return joinPoint.proceed();
        }
        return MDCTemplate.traceMethod(() -> {
            try {
                return joinPoint.proceed();
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        }, signatureName, new MdcInfo(MDC.types(), MDC.refresh()));
    }
}

3.8 @MDC 注解与 TraceType追踪类型枚举

MDC注解

java 复制代码
/**
 * MDC 注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MDC {
    TraceType[] types() default TraceType.LOG;

    boolean refresh() default true;
}

TraceType追踪类型枚举

java 复制代码
public enum TraceType {
    LOG,      // 日志:记录出入参
    TIMER,    // 计时:记录方法耗时
    METRICS,  // 指标:上报监控指标
    EVENT     // 事件:记录关键事件
}

使用示例

java 复制代码
@MDC(types = {TraceType.TIMER, TraceType.LOG})
public ResponseDTO request(RequestDTO requestDTO) {
    // MDCAspect 拦截 → MDCTemplate.traceMethod()
    // 1. 生成 traceId,设置到 MDC + ContextHolder
    // 2. 记录 parentTraceId / preTraceId 关联日志
    // 3. 执行业务方法
    // 4. finally: 清理 MDC,记录耗时
}

3.9 NanoIdUtils --- traceId生成工具

源码

java 复制代码
public final class NanoIdUtils {
    public static final SecureRandom DEFAULT_NUMBER_GENERATOR = new SecureRandom();
    public static final char[] DEFAULT_ALPHABET = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
    public static final int DEFAULT_SIZE = 21;

    private NanoIdUtils() {
    }

    public static String randomNanoId() {
        return randomNanoId(DEFAULT_NUMBER_GENERATOR, DEFAULT_ALPHABET, 21);
    }

    public static String randomNanoId(Random random, char[] alphabet, int size) {
        if (random == null) {
            throw new IllegalArgumentException("random cannot be null.");
        } else if (alphabet == null) {
            throw new IllegalArgumentException("alphabet cannot be null.");
        } else if (alphabet.length != 0 && alphabet.length < 256) {
            if (size <= 0) {
                throw new IllegalArgumentException("size must be greater than zero.");
            } else {
                int mask = (2 << (int)Math.floor(Math.log((alphabet.length - 1)) / Math.log(2.0D))) - 1;
                int step = (int)Math.ceil(1.6D * (double)mask * (double)size / (double)alphabet.length);
                StringBuilder idBuilder = new StringBuilder();

                while(true) {
                    byte[] bytes = new byte[step];
                    random.nextBytes(bytes);

                    for(int i = 0; i < step; ++i) {
                        int alphabetIndex = bytes[i] & mask;
                        if (alphabetIndex < alphabet.length) {
                            idBuilder.append(alphabet[alphabetIndex]);
                            if (idBuilder.length() == size) {
                                return idBuilder.toString();
                            }
                        }
                    }
                }
            }
        } else {
            throw new IllegalArgumentException("alphabet must contain between 1 and 255 symbols.");
        }
    }
}

4. 传递链路全景

复制代码
主线程(HTTP 请求 / XXL-JOB 调度)
  │
  │  ① @MDC 注解触发 MDCAspect 切面
  │     → MDCTemplate.traceMethod()
  │     → 生成 traceId(NanoId),设置到 MDC + ContextHolder (ThreadLocal)
  │
  │  ② 业务代码提交任务到线程池
  │     → DecoratorThreadPoolExecutor.execute(command)
  │     → ContextTaskDecorator.decorate(command)
  │     → ②a 捕获主线程 Context(含 traceId)
  │     → ②b 包装为新的 Runnable
  │
  ▼
子线程(线程池工作线程)
  │
  │  ③ ContextTaskDecorator 包装的 Runnable 执行
  │     → before(parentCxt): 获取/创建子线程 Context,设置 parent
  │     → ContextHolder.getOrBuildContext() 得到 childCxt
  │     → childCxt.setParent(parentCxt)
  │
  │  ④ 业务逻辑执行
  │     → ContextHolder.getContext() 可获取上下文
  │     → context.getParentTraceId() 获取父线程 traceId
  │     → context.getTraceId() 获取自身 traceId(如需通过 @MDC 生成)
  │
  │  ⑤ 任务执行完毕
  │     → finally: ContextHolder.remove() 清理 ThreadLocal
  │     → 防止线程复用时上下文泄漏
  │
  ▼
日志关联查询
  │
  ├── ",start,parentTraceId=[xxx]" → 查找由主线程派生的子线程日志
  └── ",start,preTraceId=[xxx]"    → 查找前序调用链路日志

5. 传递机制详解

5.1 第一层:主线程 traceId 生成

java 复制代码
// 方法标注 @MDC 注解
@MDC(types = {TraceType.TIMER, TraceType.LOG})
public ResponseDTO request(RequestDTO requestDTO) {
    // MDCAspect 拦截 → MDCTemplate.traceMethod()
    // 1. 从 MDC 获取 preTraceId(前序调用链路的 traceId)
    // 2. 从 ContextHolder 获取/创建 Context
    // 3. 生成 traceId(NanoId),设置到 Context + MDC
    // 4. 如果 parentTraceId 不为空,记录 "xxx start,parentTraceId=[yyy]" 日志
    // 5. 如果 preTraceId 不为空,记录 "xxx start,preTraceId=[yyy]" 日志
    // 6. 执行业务方法
    // 7. finally: 清理 MDC,恢复 preTraceId
}

5.2 第二层:ContextTaskDecorator 自动透传

复制代码
提交线程(父线程)               执行线程(子线程)
   │                              │
   │  ContextHolder.getOrBuildContext()
   │  → 获取主线程 Context(含 traceId)
   │                              │
   │  ──── 包装 Runnable ────▶
   │                              │
   │                              │  before(parentCxt)
   │                              │  → childCxt = ContextHolder.getOrBuildContext()
   │                              │  → childCxt.setParent(parentCxt)
   │                              │
   │                              │  runnable.run()
   │                              │  → 业务逻辑执行
   │                              │  → ContextHolder.getContext() 获取上下文
   │                              │
   │                              │  finally: ContextHolder.remove()
   │                              │  → 清理 ThreadLocal

闭包捕获机制详解

java 复制代码
public Runnable decorate(Runnable runnable) {
    // 此处运行在父线程
    // parentCxt 是一个普通的 Java 对象引用,保存在局部变量中
    Context parentCxt = ContextHolder.getOrBuildContext();

    // 返回的 lambda 表达式捕获了 parentCxt 这个局部变量
    // 当 lambda 在子线程执行时,parentCxt 引用仍然指向父线程创建的 Context 对象
    return () -> {
        try {
            before(parentCxt);  // 子线程通过闭包访问 parentCxt
            runnable.run();
        } finally {
            ContextHolder.remove();
        }
    };
}

关键原理 :Java lambda 表达式捕获外部局部变量时,实际上是捕获了变量的引用副本。parentCxt 是一个堆上的 Context

对象,引用可以从父线程通过闭包"携带"到子线程,这绕过了 ThreadLocal 不能跨线程传递的限制。

5.3 第三层:嵌套线程池传递

当子线程再次提交任务到另一个线程池时,ContextTaskDecorator 会自动捕获子线程的 Context(已包含 parent 链),形成 *

Context 父子链*:

复制代码
主线程 Context(traceId=A)
  └─▶ 子线程 Context(traceId=B, parent=Context(A))
       └─▶ 孙线程 Context(traceId=C, parent=Context(B))
            └─▶ context.getParentTraceId() → B
                context.getParent().getParentTraceId() → A

这意味着

  • 每层线程池的 ContextTaskDecorator 都会自动捕获当前线程的 Context
  • 子线程的 Context 已经有 parent 链,再次捕获时整个链都会被传递
  • 可以通过 parent 链追溯完整的调用层级

6. 日志关联查询

6.1 关键字段说明

日志关键字段 含义 查询场景
traceId 当前线程的 traceId 精确查找某个请求链路
parentTraceId 父线程的 traceId 查找由主线程派生的子线程日志
preTraceId 前序调用链路的 traceId(MDC 中原有的 traceId) 查找前序调用链路日志

6.2 日志格式示例

复制代码
主线程:  SyncSingleService request start,traceId=[abc123]
子线程:  BatchProcessor process start,parentTraceId=[abc123]
子线程:  BatchProcessor process start,preTraceId=[abc123]

6.3 查询方式

查询时使用关键字 ",start,parentTraceId=[abc123]" 可找到所有由主线程 abc123 派生的子线程日志。

6.4 日志输出位置

关联日志在 MDCTemplate.traceMethod() 中输出:

java 复制代码
if(parentTraceId !=null){
        // 标识线程池子任务与提交线程的关联关系
        log.info("{} start,parentTraceId=[{}] ",signatureName, parentTraceId);
}
        if(preTraceId !=null){
        // 标识同一调用链中前序方法的 traceId 关联
        log.info("{} start,preTraceId=[{}] ",signatureName, preTraceId);
}

7. SkyWalking 集成

7.1 集成架构总览

SkyWalkingUtil 是 SkyWalking APM 与线程池体系的桥接工具,解决 SkyWalking Span 无法自动跨线程继承 的问题:ContextManager 的链路上下文(Segment/Span)绑定在线程上,线程池中的工作线程无法感知父线程的链路状态。

工具类通过 "捕获 → 继承 → 销毁"三段式流程 (与 SkyWalking 官方 Cross Thread Propagation 一致),配合 Context.snapshot 字段实现跨线程链路继承:

复制代码
父线程(提交线程)                    子线程(工作线程)
    │                                    │
    │ ① addContextSnapshot(context)       │
    │    → ContextManager.capture()       │
    │    → 快照存入父 Context.snapshot     │
    │                                    │
    │ ───── 闭包携带快照 ────▶           │
    │                                    │ ② continueSpan(child, name, parentCxt)
    │                                    │    → ContextManager.continued(snapshot)  恢复父链路
    │                                    │    → ContextManager.createLocalSpan()   创建子 Span
    │                                    │    → capture() 刷新子 Context 快照(支持多层嵌套)
    │                                    │
    │                                    │ ③ 业务逻辑执行(挂载在子 LocalSpan 下)
    │                                    │
    │                                    │ ④ finally: endSpan()
    │                                    │    → ContextManager.stopSpan() 结束子 Span

7.2 方法详解(基于最新源码)

方法 执行位置 功能 关键设计
addContextSnapshot(Context) 父线程 捕获当前 SkyWalking Span 快照,绑定到父 Context 幂等:Context 为空或已有快照时直接返回;无 Agent 时安全降级
continueSpan(Context, String, Context) 子线程 恢复父线程快照并创建子 LocalSpan,实现跨线程链路继承 continued() 恢复、再 createLocalSpan(),顺序不可颠倒;spanName 空时兜底默认名称
endSpan() 子线程 finally 结束当前子 LocalSpan isActive() 判断后 stopSpan(),无 Agent 时安全降级

源码

java 复制代码
/**
 * SkyWalking 链路上下文 创建 / 继承 / 销毁工具
 *
 * <p>跨线程链路继承标准流程(与 SkyWalking 官方 Cross Thread Propagation 一致):</p>
 * <ol>
 *     <li>父线程:{@link #addContextSnapshot(Context)} 捕获父线程 Span 快照,绑定到父 Context</li>
 *     <li>子线程:{@link #continueSpan(Context, String, Context)} 先恢复父线程快照,再创建子 LocalSpan</li>
 *     <li>子线程 finally:{@link #endSpan()} 结束子 LocalSpan</li>
 * </ol>
 *
 * <p>注意:本工具依赖以 -javaagent 方式启动的 SkyWalking Agent。
 * 无 Agent 或 Agent 异常时,所有方法安全降级(记录 debug 日志),不影响业务执行。</p>
 *
 * @author liteng
 */
@Slf4j
public class SkyWalkingUtil {

    /**
     * 默认 Span 名称,spanName 为空时兜底
     */
    private static final String DEFAULT_SPAN_NAME = "thread-pool-execute";

    /**
     * 在父线程中调用:捕获当前 SkyWalking Span 快照并绑定到 Context,
     * 供子线程通过 continueSpan 恢复,实现跨线程链路继承。
     * 幂等操作:Context 为空或已有快照时直接返回。
     *
     * @param context 父线程 Context
     */
    public static void addContextSnapshot(Context context) {
        if (context == null || context.getSnapshot() != null) {
            return;
        }
        try {
            if (ContextManager.isActive()) {
                context.setSnapshot(ContextManager.capture());
            }
        } catch (Exception e) {
            // 无 Agent 或 Agent 未初始化时安全降级,不影响业务
            log.debug("capture SkyWalking snapshot failed, skip, context={}", context, e);
        }
    }

    /**
     * 在子线程中调用:恢复父线程快照并创建子 LocalSpan,实现跨线程链路继承。
     *
     * <p>时序说明:必须先 {@link ContextManager#continued(ContextSnapshot)} 恢复父上下文,
     * 再 {@link ContextManager#createLocalSpan(String)} 创建子 Span,顺序不可颠倒;
     * 否则子 Span 无法挂载到父链路上,将成为孤儿 Span。</p>
     *
     * @param context   子线程 Context(可空;非空时刷新快照,供更深层子线程继续继承)
     * @param spanName  子线程 Span 名称(可空,空时使用默认名称)
     * @param parentCxt 父线程 Context(可空;无快照时不建立继承关系,仅创建本地 Span)
     */
    public static void continueSpan(Context context, String spanName, Context parentCxt) {
        try {
            ContextSnapshot parentSnapshot = parentCxt == null ? null : parentCxt.getSnapshot();
            // ① 先恢复父线程快照,再创建子 Span(顺序不可颠倒)
            if (parentSnapshot != null) {
                ContextManager.continued(parentSnapshot);
            }
            AbstractSpan span = ContextManager.createLocalSpan(isBlank(spanName) ? DEFAULT_SPAN_NAME : spanName);
            if (span != null) {
                span.setComponent(ComponentsDefine.JDK_THREADING);
            }
            // ② 恢复后重新捕获当前快照(含父链路信息),刷新子 Context,支持多层线程池嵌套继承
            if (context != null) {
                context.setSnapshot(ContextManager.capture());
            }
        } catch (Exception e) {
            // 无 Agent 或链路异常时安全降级,不影响业务执行
            log.debug("continue SkyWalking span failed, skip, spanName={}", spanName, e);
        }
    }

    /**
     * 在子线程 finally 中调用:结束当前 Span。
     */
    public static void endSpan() {
        try {
            if (ContextManager.isActive()) {
                ContextManager.stopSpan();
            }
        } catch (Exception e) {
            // 无 Agent 或链路异常时安全降级,不影响业务执行
            log.debug("stop SkyWalking span failed, skip", e);
        }
    }

    private static boolean isBlank(String str) {
        return str == null || str.trim().isEmpty();
    }
}

7.3 跨线程链路继承时序(核心流程)

复制代码
父线程(提交任务)
  │
  │  addContextSnapshot(parentCxt)
  │    ├─ ContextManager.isActive()?       ← 当前是否有活动 Span
  │    │    └─ 是 → ContextManager.capture() 捕获快照
  │    │    └─ 否 → 跳过(无线索可继承)
  │    └─ parentCxt.snapshot = 快照
  │
  ▼
子线程(执行任务)
  │
  │  continueSpan(childCxt, spanName, parentCxt)
  │    ├─ parentCxt.snapshot != null?
  │    │    ├─ 是 → ContextManager.continued(snapshot)  ← ① 先恢复父链路(顺序关键!)
  │    │    └─ 否 → 跳过(无快照不建立继承,仅创建本地 Span)
  │    ├─ ContextManager.createLocalSpan(spanName)      ← ② 再创建子 Span
  │    │    └─ span.setComponent(JDK_THREADING) 标记为线程池执行组件
  │    └─ childCxt.snapshot = ContextManager.capture()  ← ③ 刷新快照,支持更深层嵌套继承
  │
  │  业务逻辑执行(日志、DB 操作等挂载在子 LocalSpan 下)
  │
  │  finally: endSpan()
  │    └─ ContextManager.isActive()?
  │         └─ 是 → ContextManager.stopSpan() 结束子 Span

为什么必须先 continued()createLocalSpan()

SkyWalking 的 createLocalSpan 会将新 Span 挂载到当前 Segment 的当前 Span 之下。如果先创建 Span 再恢复快照,恢复操作会切换 Segment,导致新创建的 Span 变成孤儿 Span ,无法挂载到父链路上。因此顺序不可颠倒:先恢复父上下文(continued),再创建子 Span(createLocalSpan

7.4 ContextTaskDecorator 中的集成点

ContextTaskDecorator 已内置 SkyWalking 集成,调用链完整覆盖 提交 → 执行 → 清理 三个阶段:

java 复制代码
@Override
public Runnable decorate(Runnable runnable) {
    // 捕获当前线程(父线程)的上下文
    Context parentCxt = ContextHolder.getOrBuildContext();
    // ① 父线程捕获 SkyWalking 快照,供子线程链路继承
    if (isSkyWalkingEnabled()) {
        SkyWalkingUtil.addContextSnapshot(parentCxt);
    }
    // 返回包装后的 Runnable(此 lambda 表达式在子线程执行)
    return () -> {
        try {
            before(parentCxt);
            runnable.run();
        } finally {
            try {
                // ② 先结束子线程 LocalSpan,再清理 ThreadLocal,避免链路残留
                if (isSkyWalkingEnabled()) {
                    SkyWalkingUtil.endSpan();
                }
                ContextHolder.remove();
            } catch (Exception e) {
                log.error("finally, enhance fail ", e);
            }
        }
    };
}

private void before(Context parentCxt) {
    try {
        Context childCxt = ContextHolder.getOrBuildContext();
        if (parentCxt != null) {
            childCxt.setParent(parentCxt);   // 建立 Context 父子关系链
        }
        // ③ 子线程恢复父线程 SkyWalking 快照并创建 LocalSpan
        if (isSkyWalkingEnabled()) {
            SkyWalkingUtil.continueSpan(childCxt, buildSpanName(), parentCxt);
        }
    } catch (Exception e) {
        log.error("before, enhance fail ", e);
    }
}

/** SkyWalking 集成是否开启(由 TraceConfig.disableSkyWalking 控制,默认开启) */
private boolean isSkyWalkingEnabled() {
    return traceConfig == null || !traceConfig.isDisableSkyWalking();
}

/** 构建子线程 Span 名称:{线程池前缀}-execute,无前缀时使用默认名称 */
private String buildSpanName() {
    if (operateName == null || operateName.trim().isEmpty()) {
        return "thread-pool-execute";
    }
    return operateName + "-execute";
}

与纯 traceId 传递的对比

阶段 traceId 传递(无 SkyWalking) SkyWalking 集成
提交(父线程) 捕获 parentCxt 存入闭包 额外执行 addContextSnapshot(parentCxt) 捕获 Span 快照
执行(子线程) childCxt.setParent(parentCxt) 额外执行 continueSpan():恢复快照 + 创建 LocalSpan
清理(子线程) ContextHolder.remove() endSpan() 结束 Span,再清理 ThreadLocal

7.5 多层嵌套继承

continueSpan 中在创建子 Span 后重新执行 context.setSnapshot(ContextManager.capture()),将包含父链路信息的当前快照 刷新到子 Context。因此当子线程再次向更深的线程池提交任务时,新的装饰器会捕获到带完整链路信息的快照,实现多层线程池嵌套继承

复制代码
主线程(Segment A)
  └─▶ 子线程 1(LocalSpan B,快照含 A)
       └─▶ 孙线程 2(LocalSpan C,快照含 A+B)
            └─▶ 曾孙线程 3(LocalSpan D,快照含 A+B+C)

SkyWalking UI 中可以看到完整的线程池调用链路树。

7.6 配置控制与安全降级

配置控制TraceConfig.disableSkyWalking 控制 SkyWalking 集成开关,ContextTaskDecorator.isSkyWalkingEnabled() 读取该配置:

java 复制代码
@Data
public class TraceConfig {
    private boolean disableSkyWalking;  // true: 禁用 SkyWalking 集成(默认 false,开启)
}

Nacos 中对应配置:

yaml 复制代码
exchange:
  biz:
    global:
      trace:
        disableSkyWalking: false   # 默认开启

安全降级机制(生产可用性的关键设计):

场景 行为 影响
未以 -javaagent 启动(无 Agent) 方法内部 try-catch,记录 debug 日志后跳过 无影响,业务正常执行
Agent 已启动但当前线程无活动 Span(如定时任务线程) isActive() 为 false,跳过捕获/停止 无线索可继承,仅创建本地 Span
ContextManager 调用异常 try-catch 捕获,记录 debug 日志 无影响,业务正常执行
disableSkyWalking = true isSkyWalkingEnabled() 为 false,完全跳过 SkyWalking 调用 无影响,仅保留 traceId 日志关联

注意 :本工具依赖以 -javaagent 方式启动的 SkyWalking Agent。无 Agent 或 Agent 异常时,所有方法安全降级,不影响业务执行。

7.7 使用注意事项

  1. Span 命名规范 :子线程 Span 名称为 {线程池前缀}-execute(如 plan-partition-batch-execute),无前缀时使用默认名 thread-pool-execute,便于在 SkyWalking UI 中按线程池维度筛选
  2. 必须在 finally 中 endSpanContextTaskDecorator 已在 finally 中调用 endSpan(),确保异常路径下 Span 也能正常结束,防止 Segment 残留
  3. 先 endSpan 再清理 ThreadLocal:清理顺序不可颠倒,否则会残留未关闭的 Span 状态
  4. 关闭开关 :如不使用 SkyWalking,将 TraceConfig.disableSkyWalking 设为 true,或迁移时移除 SkyWalkingUtilContext.snapshot 字段(详见 [§9 trace 组件移植指南](#§9 trace 组件移植指南))

8. 传递注意事项

  1. 必须使用 ContextTaskDecorator :在构建线程池时传入 taskDecorator,否则 traceId 不会自动透传

  2. finally 必须清理ContextTaskDecorator 已在 finally 中调用 ContextHolder.remove(),自定义任务不应额外清理

  3. 嵌套线程池传递 :子线程再次提交任务到另一个线程池时,traceId 会自动通过 Context.parent 链传递

  4. 日志格式规范 :跨线程日志使用 parentTraceId=[xxx]preTraceId=[xxx] 格式输出

  5. 异常兜底ContextTaskDecoratorbefore()finally 块都有 try-catch,装饰器异常不影响业务执行

  6. @MDC 注解位置@MDC 注解应标注在需要追踪的方法上(如 Service 入口方法),AOP 切面会自动拦截

  7. refresh 配置@MDC(refresh = true)(默认)每次生成新的 traceId;refresh = false 时如果 Context 中已有 traceId 则复用


9. trace 组件移植指南

9.1 需要迁移的文件清单

复制代码
com.xxx.trace.juc/
├── DecoratorThreadPoolExecutor.java  ← 必须迁移
├── ContextTaskDecorator.java         ← 必须迁移(核心组件)
├── TaskDecorator.java                ← 必须迁移
└── SkyWalkingUtil.java               ← 可选迁移(如使用 SkyWalking)

com.xxx.trace/
├── Context.java                      ← 必须迁移
├── ContextHolder.java               ← 必须迁移
├── MDCTemplate.java                  ← 必须迁移
├── MdcInfo.java                     ← 必须迁移
├── TraceConfig.java                 ← 必须迁移
├── annotations/MDC.java             ← 必须迁移
├── aspect/MDCAspect.java            ← 必须迁移
└── enums/TraceType.java             ← 必须迁移

com.xxx.tools/
└── NanoIdUtils.java                 ← 必须迁移(traceId 生成工具)

9.2 外部依赖

依赖 用途 是否必须
org.apache.skywalking:apm-agent-core SkyWalking 链路追踪 否(可选,由 TraceConfig.disableSkyWalking 控制)
com.alibaba:fastjson @JSONField 注解 是(或替换为 @JsonIgnore
org.projectlombok:lombok @Data / @Slf4j / @Builder
org.springframework:spring-aop AOP 切面支持
org.aspectj:aspectjweaver AOP 切面支持
org.slf4j:slf4j-api MDC 操作

9.3 迁移步骤

  1. 复制源码:将上述文件清单中的 .java 文件复制到目标项目的 common 模块
  2. 适配包名 :将 com.xxx 替换为目标项目的包前缀
  3. 处理 SkyWalking 依赖
    • 如有 SkyWalking:保留 SkyWalkingUtil
    • 如无 SkyWalking:删除 SkyWalkingUtil.java,移除 Context.snapshot 字段和 TraceContextWrap.java
  4. 配置 Spring 扫描 :确保 com.jzsk.trace.aspect.MDCAspect 被 Spring 扫描到(@Component + @Aspect
  5. 集成到线程池 :确保 BaseInfraExecutorFactory.buildThreadPoolExecutor() 传入了 ContextTaskDecorator 实例

9.4 最小可用版本(无 SkyWalking)

如果目标项目不需要 SkyWalking,trace 相关组件需要迁移以下文件:

复制代码
trace 核心(8 个文件):
  Context.java(移除 snapshot 字段)
  ContextHolder.java
  MDCTemplate.java
  MdcInfo.java
  TraceConfig.java
  annotations/MDC.java
  aspect/MDCAspect.java
  enums/TraceType.java

装饰器(3 个文件):
  DecoratorThreadPoolExecutor.java
  ContextTaskDecorator.java
  TaskDecorator.java

工具(1 个文件):
  NanoIdUtils.java(traceId 生成)

共 12 个文件即可实现完整的 traceId 跨线程透传能力。

如需同时迁移线程池架构组件,请参阅 企业级 Java 线程池架构全解:从设计理念到生产实践与项目移植 的移植指南。合计约 22

个文件可实现完整的线程池管理 + traceId 透传能力。


10. 故障排查

10.1 traceId 丢失

现象:子线程日志中 traceId 为空或与主线程不一致

排查步骤

  1. 确认线程池创建方式 :确认线程池是否通过 ThreadPoolExecutorBuilder.build() 创建(产出
    DecoratorThreadPoolExecutor
  2. 确认 taskDecorator 注入 :确认构建时传入了 taskDecoratorContextTaskDecorator 实例)
  3. 确认 before 方法执行 :确认 ContextTaskDecorator.decorate()before() 方法正确设置了 parent
  4. 确认 finally 清理 :确认 finallyContextHolder.remove() 被执行(检查是否有异常跳过 finally)

解决方式

  • 不使用 new ThreadPoolExecutor(),必须使用 ThreadPoolExecutorBuilder
  • 确保 BaseInfraExecutorFactory.buildThreadPoolExecutor() 传入了 ContextTaskDecorator

排查代码路径

复制代码
BaseInfraExecutorFactory.afterPropertiesSet()
  → buildThreadPoolExecutor(prefix, new ContextTaskDecorator(prefix, getTraceConfig()))
    → ThreadPoolExecutorBuilder.taskDecorator(contextTaskDecorator)
      → build() → DecoratorThreadPoolExecutor(taskDecorator)
        → execute() → taskDecorator.decorate(command)
          → ContextTaskDecorator.decorate()
            → ContextHolder.getOrBuildContext()  ← 捕获
            → before(parentCxt)                  ← 恢复
            → finally: ContextHolder.remove()    ← 清理

10.2 traceId 串号

现象:子线程日志中 traceId 与其他请求的 traceId 混淆

排查步骤

  1. 确认 finally 清理ContextTaskDecoratorfinally 块是否正确执行了 ContextHolder.remove()
  2. 检查自定义清理 :业务代码是否在 ContextTaskDecorator 之外手动清理了 ThreadLocal,导致清理时机错乱
  3. 检查线程池复用 :确认是否有线程池外的代码直接操作了 ContextHolder

解决方式

  • 不要在业务代码中手动调用 ContextHolder.remove(),清理由 ContextTaskDecorator 统一管理
  • 确认 ContextTaskDecoratorfinally 块中 try-catch 是否吞掉了异常

10.3 嵌套传递断裂

现象:孙线程日志中无法获取子线程的 traceId

排查步骤

  1. 确认嵌套线程池也使用了装饰器 :子线程提交任务到另一个线程池时,该线程池也需要通过 ThreadPoolExecutorBuilder 创建并注入
    ContextTaskDecorator
  2. 确认 Context 父子链 :检查 before() 方法中 childCxt.setParent(parentCxt) 是否正确执行
  3. 确认 ContextHolder 状态 :子线程在提交嵌套任务前,ContextHolder.getContext() 不应为 null

解决方式

  • 所有线程池统一通过 BaseInfraExecutorFactory 创建,确保都注入了 ContextTaskDecorator
  • 检查 ContextTaskDecorator.before() 中的异常日志,确认没有吞掉设置 parent 的异常
相关推荐
程序员黑豆1 小时前
Java入门第一步:从零开始编写你的第一个Hello World程序
java·前端·ai编程
Jazz_z2 小时前
如何用Python将彩色PDF一键转为黑白(灰度)
java·python·pdf
青山木2 小时前
Hot 100 --- 在排序数组中查找元素的第一个和最后一个位置
java·数据结构·算法·leetcode
天空之外1362 小时前
Docker安装MySQL 8.4 LTS、Docker安装Redis 7.4.x、Docker安装MinIO
java·mysql·docker
fīɡЙtīиɡ ℡2 小时前
深入学习JAVA并发编程(上)
java·开发语言·学习
XS0301063 小时前
Spring AI:两种内置 Advisor 快速实现 RAG
java·人工智能·spring
mifengxing3 小时前
LeetCode 238 除自身以外数组的乘积|无除法O(n)时间+O(1)空间双解法
java·算法·leetcode
godwaskillde3 小时前
Java 11 HttpClient如何配置HTTP代理?连接测试与异常处理示例
java·开发语言·http
小林敲代码77884 小时前
Spring Boot 3 升级踩坑:aj-captcha 行为验证码底图加载失效
java·spring boot·后端