如何在 Spring 'HandlerMethodArgumentResolver' 中多次读取请求正文?

Posted

技术标签:

【中文标题】如何在 Spring \'HandlerMethodArgumentResolver\' 中多次读取请求正文?【英文标题】:How can I read request body multiple times in Spring 'HandlerMethodArgumentResolver'?如何在 Spring 'HandlerMethodArgumentResolver' 中多次读取请求正文? 【发布时间】:2016-04-20 15:24:13 【问题描述】:

我正在尝试解析RequestMapping 方法的某些参数,以从请求正文中提取值并验证它们并将它们注入某些带注释的参数中。

@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                              NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception 
    // 1, get corresponding input parameter from NativeWebRequest
    // 2, validate
    // 3, type convertion and assemble value to return
    return null;

最大的问题是我发现HttpServletRequest(get from NativeWebRequest) 无法读取输入流(某些参数在请求正文中)不止一次。那么如何多次检索Inputstream/Reader或请求正文呢?

【问题讨论】:

一种解决方案是使用 ThreadLocal 将参数存储在来自请求的过滤器中,然后在代码中的任何位置多次使用它们。 @SandeepPoonia 这可能会有所帮助。但一个问题是,如果我将正文保存到线程本地(通过调用 HttpServletRequest.getReader/getInputStream),则永远不会再次调用它。控制器层的事件,我不能声明“@RequestBody String body”(可能会被Spring抛出异常),因为Spring不能再读取输入流了。 【参考方案1】:

您可以添加过滤器,截取当前的HttpServletRequest 并将其包装在自定义HttpServletRequestWrapper 中。在您的自定义HttpServletRequestWrapper 中,您读取请求正文并将其缓存,然后实现getInputStreamgetReader 以从缓存值中读取。由于包装请求后,缓存值始终存在,您可以多次读取请求正文:

@Component
public class CachingRequestBodyFilter extends GenericFilterBean 
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException 
        HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
        MultipleReadHttpRequest wrappedRequest = new MultipleReadHttpRequest(currentRequest);
        chain.doFilter(wrappedRequest, servletResponse);
    

经过这个过滤器,大家会看到wrappedRequest具有被多次读取的能力:

public class MultipleReadHttpRequest extends HttpServletRequestWrapper 
    private ByteArrayOutputStream cachedContent;

    public MultipleReadHttpRequest(HttpServletRequest request) throws IOException 
        // Read the request body and populate the cachedContent
    

    @Override
    public ServletInputStream getInputStream() throws IOException 
        // Create input stream from cachedContent
        // and return it
    

    @Override
    public BufferedReader getReader() throws IOException 
        // Create a reader from cachedContent
        // and return it
    

为了实现MultipleReadHttpRequest,你可以看看spring框架中的ContentCachingRequestWrapper,它基本上是做同样的事情。

这种方法有其自身的缺点。首先,它有点低效,因为对于每个请求,请求正文至少被读取两次。另一个重要的缺点是,如果您的请求正文包含 10 GB 价值的流,您会读取 10 GB 数据,更糟糕的是会将其放入内存以供进一步检查。

【讨论】:

是的,这是迄今为止最好的解决方案。但是效率确实是个大问题。我想我必须找到一种方法来限制输入流的大小,以防有人进行破坏或其他事情。至于请求正文至少读取两次,我认为这是可以接受的,因为我只接受来自请求正文的 json 格式的输入字符串,通常小于 1kb/pr。 此答案中提供了完整示例:***.com/a/17129256/364401 顺便说一句,ContentCachingRequestWrapper 不做同样的事情,它允许从缓存中获取数据,但您必须首先读取真实数据。它对记录很有用,但对其他事情不太好。 我想将它用于 multipart/form-data 但 ContentCachingRequestWrapper 也不支持 一个有趣的事实是,出于某种原因,Spring 的ContentCachingRequestWrapper 并没有真正“缓存”任何东西(正如我通过试验和错误发现的那样)。相反,@Stim 在姊妹线程中引用的解决方案运行良好 - 需要您实现自己的 MultiReadHttpServletRequest 包装器。

以上是关于如何在 Spring 'HandlerMethodArgumentResolver' 中多次读取请求正文?的主要内容,如果未能解决你的问题,请参考以下文章

spring mvc的RequestMappingHandlerMapping注册HandlerMethod源码分析

源码剖析Spring MVC如何将请求映射到Controller?

Spring Boot 如何获取 Controller 方法名和注解信息?

Spring Boot 如何获取 Controller 方法名和注解信息?

如何实现@ResponseBody,把Json字符串转换为指定类型

Spring Boot 如何获取 Controller 方法名和注解信息?