在典型的 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");
}
}
日志记录配置。
<Pattern>%d %-5level %X{requestId} [%t] %C{10}:%L %m%n</Pattern>
示例记录。
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'
使用 Java 21+ 中的虚拟线程,我的印象是线程可以在等待任何 IO 时自动挂起请求,并且线程可以开始处理其他请求。在这种情况下,当线程开始服务其他请求时,日志记录 MDC 似乎可以“渗入”其他请求日志。如何解决这个问题,以便我可以继续将唯一值添加到每个请求的日志记录语句中?
如果您使用的 MDC 适配器是使用
ThreadLocal
的适配器,例如LogbackMDCAdapter
,那么您发布的过滤器将适用于虚拟线程和平台线程。如果您的虚拟线程将被挂起,然后在另一个 Carrier(平台)线程上继续,那么它的 ThreadLocal
将被正确传输(至少 Project Loom 如此承诺)。
虚拟线程文档的
虚拟线程支持线程局部变量,就像平台线程一样
但是,虚拟线程的用户会被警告不要在虚拟线程上过度使用
ThreadLocal
(这很好理解,因为这些ThreadLocal
必须在虚拟线程的Carrier线程之间传输 - 这是非常粗略地了解“幕后”发生的情况)。
相反,Project Loom 建议使用
ScopedValue
。但是,在您的问题范围内,1)应该在虚拟线程生成时启动 ScopedValue
的使用,即 Servlet 容器中的某个位置(如果使用 Tomcat 那么它将是一个连接器),2)一个特殊的应该使用面向 ScopedValue
的实现 3) MDCAdapter
仍然是 Java 21 中的预览。 How to propagating context through StructuredTaskScope by ScopedValue 中已经列出了一些这个方向的计划...怎么样StructuredTaskScope 中的 MDC ThreadContextMap?。在我看来,随着
ScopedValue
的设计,工作应该在两个层面上进行:Servlet 容器(虚拟线程在其中产生)和特殊的 ScopedValue
实现。