在使用虚拟线程(Executor)时,MDC与安全上下文的传播会失败

后端开发 2026-07-11

我正在把一个Spring Boot 3.x应用迁移到使用Java虚拟线程。我已经把标准异步执行器替换为虚拟线程执行器:

@Bean
public Executor taskExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}

当一个请求到来时,过滤器会成功填充 SecurityContextHolder 和SLF4J的 MDC(带着一个 traceId)。然而,当我使用 @AsyncCompletableFuture.supplyAsync(..., taskExecutor) 将任务下放时,子虚拟线程会同时丢失MDC上下文和认证对象。

我知道对于标准ThreadPool,可以使用 TaskDecorator,但由于虚拟线程不应该被池化,注入一个标准装饰器似乎是一种反模式。我也查看过 micrometer-context-propagation 库,但我不确定如何在Spring生成的所有虚拟线程上全局接入,而不必为每个 Runnable 手动包裹。

在Spring Boot 3中,自动将 ThreadLocal 值(MDC、SecurityContext)传播到虚拟线程的规范、生产就绪的方式是什么?无需为每次异步调用编写样板的上下文包装代码?

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@SpringBootApplication
public class VirtualThreadContextApp {

    public static void main(String[] args) {
        SpringApplication.run(VirtualThreadContextApp.class, args);
    }

    // 1. Define the Virtual Thread Executor (Java 21+)
    @Bean
    public ExecutorService virtualThreadExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

@RestController
class ContextTestController {
    private static final Logger log = LoggerFactory.getLogger(ContextTestController.class);
    private final ExecutorService executor;

    ContextTestController(ExecutorService virtualThreadExecutor) {
        this.executor = virtualThreadExecutor;
    }

    @GetMapping("/test-context")
    public String testContextLoss() throws Exception {
        // 2. Simulate a Web Filter setting MDC and SecurityContext
        MDC.put("traceId", "req-12345");
        SecurityContextHolder.getContext().setAuthentication(
            new UsernamePasswordAuthenticationToken("demo-user", "password")
        );

        log.info("Main Thread - traceId: {}, user: {}", 
                 MDC.get("traceId"), 
                 SecurityContextHolder.getContext().getAuthentication().getName());

        // 3. Offload work to a Virtual Thread
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {

            // BUG: Context is lost here! MDC is null, SecurityContext is empty.
            String childTraceId = MDC.get("traceId");
            String childUser = SecurityContextHolder.getContext().getAuthentication() != null ? 
                               SecurityContextHolder.getContext().getAuthentication().getName() : "NULL";

            log.info("Virtual Thread - traceId: {}, user: {}", childTraceId, childUser);

        }, executor);

        future.get(); // Block to wait for completion

        // Cleanup
        MDC.clear();
        SecurityContextHolder.clearContext();

        return "Check console logs to see context loss.";
    }
}

解决方案

原生MDC与 Spring Security的解决方案是

    @Bean
    public ExecutorService virtualThreadExecutor() {
        return Executors.newThreadPerTaskExecutor(r -> {
        final Map<String, String> map = MDC.getCopyOfContextMap();
        final SecurityContext context = SecurityContextHolder.getContext();
            return Thread.ofVirtual().unstarted(() -> {
        MDC.setContextMap(map);
        SecurityContextHolder.setContext(context);
        try {
            r.run();
        } finally {
            MDC.clear();
            SecurityContextHolder.clearContext();
        }
            });
        });
    }

这当然没有基于Observation/Micrometer的方案那样优雅、强大。

在Spring Boot 3中,自动将ThreadLocal值(MDC、SecurityContext)传播到虚拟线程的规范、生产就绪的方式是什么?

这是一个很好的问题。

首先,上下文传播,以及我们最近讨论的 [Graceful Shutdown],与池化是正交的(独立的),即便虚拟线程不建议进行池化,上下文传播的问题仍然是一样的:在调用一个 Runnable 任务时,必须检索它的状态,然后把它设置/复制到执行该任务的线程上,虚拟线程还是平台线程,均无所谓。因此,你所问的传播到虚拟线程的规范做法,基本上和传统的平台线程相同。

另一方面,正如从 [虚拟线程指南] 可以推断的,虚拟线程的原生传播方式,是使用 ScopedValues而不是 ThreadLocals。然而,像MDC和 Spring Security这样的框架并不非常热衷于采用这一思路。这个话题在Stack Overflow的讨论中有专门的条目,特别是 [Spring Security Virtual Threads and ThreadLocal](Spring Security)以及 [Logback: availability of MDCs in forks created inside a StructuredTaskScope](MDC);你也可以查看我为这两种情况整理的自制解决方案。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章