【Spring】全局异常处理中,如何获取请求体内容?

背景

在SpringMVC项目中,通常会使用@ExceptionHandler方法,对Controller做统一的全局异常处理。当发生异常时,希望对异常的上下文做日志记录,特别是HTTP请求信息,如请求的URL、请求参数。对于POST请求,主要信息在请求体里,因此还希望能记录请求体内容。

问题

HTTP请求信息,一般通过HttpServletRequest对象(request)获取。然而,在按常规思路,通过request获取请求体内容时,却会发生异常。

有问题的全局异常处理写法如下所示。

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(HttpServletRequest request,
                                                  Exception e) {
        log.error("Global exception: {} [uri: {}] [query: {}] [body: {}]", e.getMessage(), request.getRequestURI(),
                request.getQueryString(), getRequestBody(request), e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error");
    }

    @SneakyThrows
    private static String getRequestBody(HttpServletRequest request) {
        // 发生异常: java.lang.IllegalStateException: getInputStream() has already been called for this request
        BufferedReader reader = request.getReader();
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    }
}

可见,请求体内容是作为一个输入流(inputStream)来处理的,只能被读取一次。在Controller层已经读取过请求体了,那么在全局异常处理就无法再次读取。

解决办法

Spring提供了一个ContentCachingRequestWrapper,用于缓存从输入流读取出来的内容。我们可以在过滤器(Filter)中,将HttpServletRequest包装成ContentCachingRequestWrapper,这样后续就可以重复获取到请求体内容了。

  1. 定义Filter:
@Component
public class RequestWrapperFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        filterChain.doFilter(new ContentCachingRequestWrapper(request), response);
    }
}
  1. 全局异常处理中,将获取请求体内容的方法改成:
    private static String getRequestBody(HttpServletRequest request) {
        ContentCachingRequestWrapper requestWrapper =
                WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (requestWrapper == null) {
            return "";
        }
        return new String(requestWrapper.getContentAsByteArray(), StandardCharsets.UTF_8);
    }

注意:使用上述方法获取请求体内容,要求请求体已经被读取过一次,否则获取到的为空。例如,在Controller的方法中,如果不传@RequestBody参数(如下所示), 就不会读取请求体,上述方法失效。

@RestController
@RequestMapping("/test")
public class TestController {

    @PostMapping("/add")
    public String add() {
        // ...
    }
}

原理分析

阅读ContentCachingRequestWrapper源码可知,它就是通过在内部维护一个cachedContent,来实现请求体内容的重复读取的。

public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {

    // ...

    private final ByteArrayOutputStream cachedContent;

    @Nullable
    private final Integer contentCacheLimit;

    @Nullable
    private ServletInputStream inputStream;

    // ...

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (this.inputStream == null) {
            this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
        }
        return this.inputStream;
    }

    // ...

    public byte[] getContentAsByteArray() {
        return this.cachedContent.toByteArray();
    }

    // ...

    private class ContentCachingInputStream extends ServletInputStream {

        private final ServletInputStream is;

        private boolean overflow = false;

        // ...

        @Override
        public int read(byte[] b) throws IOException {
            int count = this.is.read(b);
            writeToCache(b, 0, count);
            return count;
        }

        private void writeToCache(final byte[] b, final int off, int count) {
            if (!this.overflow && count > 0) {
                if (contentCacheLimit != null &&
                        count + cachedContent.size() > contentCacheLimit) {
                    this.overflow = true;
                    cachedContent.write(b, off, contentCacheLimit - cachedContent.size());
                    handleContentOverflow(contentCacheLimit);
                    return;
                }
                cachedContent.write(b, off, count);
            }
        }

        // ...
    }
}

外部在调用ContentCachingRequestWrappergetInputStream()方法时,实际上获取到的是其内部类ContentCachingInputStream的对象。该内部类继承自ServletInputStream,当它的read()方法被调用时,请求体内容会被写入缓存cachedContent。于是,通过ContentCachingRequestWrappergetContentAsByteArray()方法,就可以获取到cachedContent的内容。

总结

SpringMVC项目中,将HttpServletRequest包装成ContentCachingRequestWrapper,可以实现对请求体内容的重复获取。这点在全局异常处理中很实用。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值