在 RestController ExceptionHandler 中记录请求正文字符串

log request body string in RestController ExceptionHandler

在 RestController 的 @ExceptionHandler.

中记录请求正文字符串

默认情况下,当请求无效时json,springboot 会抛出一个HttpMessageNotReadableException,但是消息非常笼统,不包括具体的请求体。这使得调查变得困难。另一方面,我可以使用过滤器记录每个请求字符串,但这样日志会被太多成功的字符串淹没。我只想在无效时记录请求。我真正想要的是 @ExceptionHandler 我会得到那个字符串(以前在某处得到)并记录为错误。

为了说明问题,我在github中创建了一个demo project

@RestController
public class GreetController {
    protected static final Logger log = LogManager.getLogger();

    @PostMapping("/")
    public String greet(@RequestBody final WelcomeMessage msg) {
        // if controller successfully returned (valid request),
        // then don't want any request body logged
        return "Hello " + msg.from;
    }

    @ExceptionHandler({HttpMessageNotReadableException.class})
    public String addStudent(HttpMessageNotReadableException e) {
        // this is what I really want!
        log.error("{the request body string got somewhere, such as Filters }"); 
        return "greeting from @ExceptionHandler";
    }
}

我曾经尝试过 HandlerInterceptor 但会出现一些错误,例如

'java.lang.IllegalStateException: Cannot call getInputStream() after getReader() has already been called for the current request'.

经过一番搜索 ,我决定将 FilterContentCachingRequestWrapper 一起使用。

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(httpServletRequest);
        chain.doFilter(cachedRequest, response);
        String requestBody = IOUtils.toString(cachedRequest.getContentAsByteArray(), cachedRequest.getCharacterEncoding());
        log.info(requestBody);
    }

除了日志在 RestController 之后,此代码运行良好。如果我改变顺序:

        String requestBody = IOUtils.toString(cachedRequest.getReader());
        log.info(requestBody);
        chain.doFilter(cachedRequest, response);

适用于无效请求,但当请求有效时,出现以下异常:

com.example.demo.GreetController : Required request body is missing: public java.lang.String com.example.demo.GreetController.greet(com.example.demo.WelcomeMessage)

我也尝试了 getContentAsByteArraygetInputStreamgetReader 方法,因为一些 tutorials 说框架检查特定方法调用。

按照@M 的建议尝试了CommonsRequestLoggingFilter。 Deinum.

但一切都是徒劳。

现在我有点困惑。谁能解释一下 RestControllerFilter 在请求有效和无效时的执行顺序? 有没有更简单的方法(更少的代码)来实现我的最终目标?谢谢!

我用的是springboot 2.6.3,jdk11.

根据@M.Deinum的评论,我解决了,希望对其他人有用:

  1. 添加过滤器
public class MyFilter implements Filter {
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(httpServletRequest);
    chain.doFilter(cachedRequest, response);
  }
}
  1. ExceptionHandler
  2. 中注入ContentCachingRequestWrapper
  @ExceptionHandler({ HttpMessageNotReadableException.class })
  public String addStudent(HttpMessageNotReadableException e, ContentCachingRequestWrapper cachedRequest) {
    log.error(e.getMessage());
    try {
      String requestBody = IOUtils.toString(cachedRequest.getContentAsByteArray(), cachedRequest.getCharacterEncoding());
      log.error(requestBody);
    } catch (IOException ex) {
      ex.printStackTrace();
    }
    return "greeting from @ExceptionHandler";
  }
  1. 创建一个过滤器,将您的请求包装在 ContentCachingRequestWrapper 中(仅此而已)。

  2. 在你的异常处理方法中使用HttpServletRequest作为参数作为参数

  3. 检查 ContentCachingRequestWrapper

    的实例
  4. 使用getContentAsByteArray获取内容。

类似这样。

public class CachingFilter extends OncePerRequestFilter {

protected abstract void doFilterInternal(
            HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

  filterChain.doFilter(new ContentCachingRequestWrapper(request), new ContentCachingResponseWrapper(response));
}

注意:我也包装了回复,以防万一你也想要。

现在在您的异常处理方法中使用 HttpServletRequest 作为参数并利用它来发挥您的优势。

@ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
  if (req instanceof ContentCachingRequestWrapper) {
    ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) req;
    log.error(new String(wrapper.getContentAsByteArray()));
  }
  return "greeting from @ExceptionHandler";
}

可能是多个过滤器向 HttpServletRequest 添加了一个包装器,因此您可能需要迭代这些包装器,您也可以使用这个

private Optional<ContentCachingRequestWrapper> findWrapper(ServletRequest req) {
    ServletRequest reqToUse = req;
    while (reqToUse instanceof ServletRequestWrapper) {
        if (reqToUse instanceof ContentCachingRequestWrapper) {
            return Optional.of((ContentCachingRequestWrapper) reqToUse);
        }
        reqToUse = ((ServletRequestWrapper) reqToUse).getRequest();
    }
    return Optional.empty();
}

你的异常处理程序看起来像这样

@ExceptionHandler({HttpMessageNotReadableException.class})
public String addStudent(HttpMessageNotReadableException e, HttpServletRequest req) {
  Optional<ContentCachingRequestWrapper) wrapper = findWrapper(req);
  wrapper.ifPresent(it -> log.error(new String(it.getContentAsByteArray())));
 
  return "greeting from @ExceptionHandler";
}

但这可能取决于您的过滤器顺序以及是否有多个过滤器添加包装器。