HttpServletRequestWrapper处理request数据流多次读取问题

问题: 要实现一个spring拦截器需要读取request数据流,但是request数据流只能读取一次,需要自己实现HttpServletRequestWrapper对数据流包装,实现如下:

public class ReadHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] cachedBytes;

    public ReadHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        IOUtils.copy(request.getInputStream(), baos);
        this.cachedBytes = baos.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(cachedBytes);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }


}

拦截器的实现:(这里不贴代码了,mock下)

public class WrapperHandlerInterceptor implements HandlerInterceptor {
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ReadHttpServletRequestWrapper requestWrapper = new ReadHttpServletRequestWrapper(request);
        //获取参数做业务逻辑
        Map parameterMap = requestWrapper.getParameterMap();
        System.out.println("####################################################parameterMap = " + JSON.toJSONString(parameterMap));
        return true;
    }
}

代码没啥特别,网上搜出来的都是这个方案,请大家关注上面的构造函数和拦截器的getParameterMap方法

线上运行一段时间正常,很快问题就来了。有个请求始终解析不到入参,但是前端确实传了参数,跟其他post请求对比了下,唯一不同就是content-type是个表单,模拟效果如下:

HttpServletRequestWrapper处理request数据流多次读取问题_第1张图片

服务端的处理代码:

HttpServletRequestWrapper处理request数据流多次读取问题_第2张图片

发现post表单请求用的是@requestparam处理的,跟其他json post请求处理不一样(@RequestBody)

既然使用的是@requestparam controller正常也应该能拿到数据才对啊,这里模拟个get请求试试会不会有这个问题

HttpServletRequestWrapper处理request数据流多次读取问题_第3张图片

get请求正常了

HttpServletRequestWrapper处理request数据流多次读取问题_第4张图片

咋回事?

表单数据处理后台要用@requestparam这个可以理解,但是当form表单内容采用 enctype=application/x-www-form-urlencoded编码时,先通过调用request.getParameter() 方法得到参数后,再调用request.getInputStream()或request.getReader()已经得不到流中的内容,因为在调用 request.getParameter()时系统可能对表单中提交的数据以流的形式读了一次,反之亦然。servlet3定义如下解释

Servlet Specifiaction 3.0:
3.1.1 When Parameters Are Available
The following are the conditions that mustbe met before post form data will be
populated to the parameter set:
1. The request is an HTTP or HTTPS request.
2. The HTTP method is POST.
3. The content type is application/x-www-form-urlencoded.
4. The servlet has made an initial call of any of the getParameterfamily of methods
on the request object.
If the conditions are not met and the post form data is not included in the parameter
set, the post data must still be available to the servlet via the request object’s input
stream. If the conditions are met, post form data will no longer be available for
reading directly from the request object’s input stream.

翻译过来就是说根据Servlet规范,如果同时满足下列条件,则请求体(Entity)中的表单数据,将被填充到request的parameter集合中(request.getParameter系列方法可以读取相关数据):
1 这是一个HTTP/HTTPS请求
2 请求方法是POST(querystring无论是否POST都将被设置到parameter中)
3 请求的类型(Content-Type头)是application/x-www-form-urlencoded
4 Servlet调用了getParameter系列方法

我们再回到最开始实现的ReadHttpServletRequestWrapper 构造函数,已开始我们就调用了getInputStream导致getParameter数据流空了,这两个操作是互斥的,然后springmvc通过@requestparam去读取属性复制的过程是getParameter,所以导致controller参数空(关于@requestparam为啥调用的是getparamters方法,可以看下HandlerMethodArgumentResolver这个源码,这个下次再做个关于参数注入源码分析).

问题的根源找到了,我们修改下Filter实现,所以网上参考的代码实现,慎用,也包括我的实现

public class ReadHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] cachedBytes;

    public ReadHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
 
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (null == this.cachedBytes) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            IOUtils.copy(request.getInputStream(), baos);
            this.cachedBytes = baos.toByteArray();
        }
        final ByteArrayInputStream bais = new ByteArrayInputStream(cachedBytes);
        return new ServletInputStream() {
            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }


}

 

补充:

关于表单getParameters()和getInputStream()方法互斥,我们可以通过Filter调用ReadHttpServletRequestWrapper(第一个错误的wapper实现)之前先调用request.getParameter("")。验证对比如下:没有调用此方法之前,request.getInputStream().available() = 1,这说明有数据,返回结果依然是参数是空异常

HttpServletRequestWrapper处理request数据流多次读取问题_第5张图片

在ReadHttpServletRequestWrapper调用前先调用下getParameter(),发现request.getInputStream().available() = 0了,,并且返回结果正常,说明inputStream的数据空了,数据在getParameters()里了

HttpServletRequestWrapper处理request数据流多次读取问题_第6张图片

HttpServletRequestWrapper处理request数据流多次读取问题_第7张图片

 

对于get请求 和 application/json的post请求不存在互斥一说,get请求的request.getInputStream()永远都是空的,不存在数据,数据在request.getParameters();而application/json请求类型的post,相反,getParameters()永远是空,数据是在request.getInputStream(),如下post请求,getInputStream()永远有数据,即使先执行了getParameter也不会存在清空getInputStream()

HttpServletRequestWrapper处理request数据流多次读取问题_第8张图片

你可能感兴趣的:(java)