利用虚拟线程记录 MDC

Rea*_*ime 5 java servlets virtual-threads

在典型的 servlet 环境中,每个请求都有自己的线程。添加日志记录 MDC 来为请求生成唯一的请求 ID 可以通过简单的 servlet 过滤器来实现。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        String requestId = UUID.randomUUID().toString();
        HttpServletResponse httpServletResponse = (HttpServletResponse)response;
        httpServletResponse.setHeader("requestId", requestId);
        MDC.put("requestId", requestId);
        chain.doFilter(request, response);
    } finally {
        MDC.remove("requestId");
    }
}
Run Code Online (Sandbox Code Playgroud)

日志记录配置。

<Pattern>%d %-5level %X{requestId} [%t] %C{10}:%L %m%n</Pattern>
Run Code Online (Sandbox Code Playgroud)

样本记录。

2024-02-04 10:29:55,160 INFO  99cd4d64-5d7c-4577-a5d3-cb8d48d1dfd5 [http-nio-8080-exec-6] c.s.q.UserController:65 Deleteing user 'test'
2024-02-04 10:29:55,260 INFO  99cd4d64-5d7c-4577-a5d3-cb8d48d1dfd5 [http-nio-8080-exec-6] c.s.q.UserController:70 Successfully deleted user 'test'
Run Code Online (Sandbox Code Playgroud)

对于 Java 21+ 中的虚拟线程,我的印象是线程可以在等待任何 IO 时自动挂起请求,并且线程可以开始处理其他请求。在这种情况下,当线程开始服务其他请求时,日志记录 MDC 似乎可以“渗入”其他请求日志。如何解决这个问题,以便我可以继续将唯一值添加到每个请求的日志记录语句中?

igo*_*.zh 5

如果您使用的 MDC 适配器是使用例如 的适配器ThreadLocalLogbackMDCAdapter那么您发布的过滤器将适用于虚拟线程以及平台线程。如果您的虚拟线程将被挂起,然后在另一个运营商(平台)线程上继续,那么它的s 将被正确传输(至少 Project Loom 如此承诺)。ThreadLocal

虚拟线程文档的“不要在线程局部变量中缓存昂贵的可重用对象”说:

虚拟线程支持线程局部变量,就像平台线程一样

然而,虚拟线程的用户被警告不要ThreadLocal在虚拟线程上过度使用 s(这可以很好地理解,因为这些ThreadLocals 必须在我们的虚拟线程的Carrier线程之间传输 - 这是正在发生的事情的非常粗略的描述”在引擎盖下”)。

相反,Project Loom 建议使用ScopedValue. 但是,在您的问题范围内,1)ScopedValue应该在虚拟线程生成时启动使用,即在Servlet容器中的某个位置(如果使用Tomcat那么它将是一个连接器)和2)一个特殊的ScopedValue面向MDCAdapter实现应该使用 3)ScopedValue仍然是 Java 21 中的预览。这个方向的一些计划已经在How to propagating context through StructuredTaskScope by ScopedValue... 中阐述了 StructuredTaskScope 中的 MDC ThreadContextMap 怎么样?。在我看来,按照ScopedValue设计,应该在两个层面上进行努力:Servlet 容器(虚拟线程在其中产生)和特殊MDCAdapter实现。