繁体   English   中英

在 Spring Boot 中的异常处理期间保留自定义 MDC 属性

[英]Preserve custom MDC attributes during exception-handling in Spring Boot

简短版本(有足够的细节)

如何保留在javax.servlet.Filter实现的doFilter()方法中添加的 MDC 属性...

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    try {
        MDC.put("token", MyToken.random()); // add the MDC attribute related to the current request processing
        chain.doFilter(request, response); // send the request to other filters and the Controller
    } finally {
        MDC.clear(); // MDC attribute must be removed so future tasks executed on the same thread would not log invalid information
    }
}

... 在异常处理期间,如果异常发生在另一个过滤器或控制器中(调用chain.doFilter(...) )。

当前,如果发生异常:将执行 finally 块,清除 MDC,然后将异常抛出过滤器。 异常处理期间的所有日志都不会包含 MDC 属性。

长版(过于详细)

我有一个简单的Filter实现来拦截所有请求。 它只创建一个随机字符串(令牌)以包含在与处理请求相关的所有日志中。

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            MDC.put("token", MyToken.random());
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }

    @Override
    public void destroy() {
    }
}

事件的顺序是:

  1. 收到请求。
  2. 我的doFilter()被调用,将随机标记添加到 MDC。
  3. 通过调用chain.doFilter()处理请求。
  4. 无论发生什么(处理完成,发生错误),MDC 都会清除finally块中的随机令牌。

问题是,如果发生错误并对其进行处理(例如通过自定义ErrorController实现),相关日志不包含令牌:

[2019.03.13 15:00:14.535] token:308...8bf [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : GET "/resource", parameters={}
[2019.03.13 15:00:14.551] token:308...8bf [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : Completed 400 BAD_REQUEST
[2019.03.13 15:00:14.551] token:          [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : "ERROR" dispatch for GET "/error", parameters={}
[2019.03.13 15:00:14.551] token:          [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to public org.springframework.http.ResponseEntity myproj.CustomErrorController.handleError(javax.servlet.http.HttpServletRequest)
[2019.03.13 15:00:14.551] token:          [ERROR] 8124 [https-jsse-nio-8443-exec-7] m.CustomErrorController                    : HTTP Error: Bad Request (400)
[2019.03.13 15:00:14.551] token:          [DEBUG] 8124 [https-jsse-nio-8443-exec-7] o.s.w.s.DispatcherServlet                  : Exiting from "ERROR" dispatch, status 400

finally块在处理它的Controller抛出异常时执行,导致清除 MDC。

在此之后执行错误处理(包括自定义ErrorController ),这意味着相关日志中没有更多令牌。

如何将自定义令牌添加到与从接收请求到发送响应的整个请求处理相关的所有日志中,包括错误处理? 我希望在线程发送响应后清除 MDC,作为最后一个操作。 无论发生什么(成功响应、错误处理期间抛出的异常等),都应该清除 MDC。

由于多个客户端同时使用 Rest 服务,日志可能会变得非常混乱。 在某个请求的整个处理过程中产生的每个日志上附加一个唯一的令牌将大大简化调试。

ServletRequestListener#requestDestroyed()而不是在Filter上清除 MDC 似乎效果很好。

这里是具体的例子。)

有两种方法可以定义为请求生成token机制。

第一种方法是定义一个过滤器并像这样包装DispatcherServlet

import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

@Component
@WebFilter
public class RequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            MDC.put("token", UUID.randomUUID().toString());
            chain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }

}

并在application.properties更改DispatcherServlet映射 url

server.servlet.context-path=/api
spring.mvc.servlet.path=/

如果您可以更改DispatcherServlet url 映射,那么这种方式是适用的,并且您应该具有这样的默认异常处理程序定义:

@ExceptionHandler({ Exception.class })
public void handleException(Exception e) {
   log.error("Error: ", e);
}

否则可以是控制台中没有令牌的日志。 如果上述条件不适用,请使用第二种方法。

第二种方式是使用Interceptor ,配置如下:

public class MDCInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        MDC.put("token", UUID.randomUUID().toString());
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        MDC.clear();
    }
}

并在配置中添加拦截器

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MDCInterceptor());
    }
}

上面的配置在postHandle方法中有MDC.clear() ,因为在异常之后afterCompletion方法会立即执行并将清除 MDC。 第二种方法涵盖了将令牌添加到日志消息的所有情况。

你可以使用 ServletRequestListener 代替,

例如:

import org.slf4j.MDC;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import java.util.UUID;

@Component
public class MyServletRequestListener implements ServletRequestListener {

    @Override
    public void requestInitialized(ServletRequestEvent requestEvent) {
        MDC.put("token", UUID.randomUUID().toString());
    }

    @Override
    public void requestDestroyed(ServletRequestEvent requestEvent) {
        MDC.clear();
    }
}

标准servlet 过滤器围绕任何 servlet 执行,包括 Spring 的DispatcherServlet (例如,参见此处),但您的过滤器是一个 Spring 组件。 由于它不使用任何 Spring bean,您可以轻松地将其转换为普通过滤器,即在web.xml配置的过滤器,如我链接的页面中所述。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM