簡體   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