【springboot进阶】HttpServletRequest输入流只能读取一次的问题

一、背景介绍

相信很多同学不知道HttpServletRequest输入流只能读取一次,因为一般的功能开发中很少会碰到以下的情况。

1)我们希望写一个过滤器,统一对请求的参数进行校验处理,就会涉及到要读取一次输入流,获取对应的参数值。

2)我们想用aop的方式,对所有controller切面中打印请求日志,所以需要在aop中获取一次请求参数。

3)我们想在项目做统一异常处理,并实现邮件通知功能,邮件内容附带上此时接口请求的参数,方便跟踪问题。

以上的这三种情况,做到后面测试的时候,同学们会发现,控制器里注入的参数居然都是空的,异常处理里面拿到的请求参数也是空的,最后发现自己前面写的东西都白做了!

在 jwt在前后端分离的最佳实践方式(二)这篇文章中,笔者也介绍过一个请求的执行顺序,大概得路径是 请求-》filter-》sevlet-》Interceptor-》aop-》controller,只要在controller前读取过一次请求流,那么后面就再拿不到请求参数了。

二、原因分析

当我们调用getInputStream()方法获取输入流时,得到的是一个InputStream对象,而实际类型是ServletInputStream,它继承InputStream。

查看InputStream的源码可以看到(这里就不贴代码,大家有兴趣可以去找具体的源码部分),读取流的时候会根据position来获取当前位置,每读取一次,该标志就会移动一次,如果读到最后,read()返回-1,表示已经读取完了。如果想要重新读取,可以调用inputstream.reset方法,但是能否reset取决于markSupported方法,返回true可以reset,反之则不行。查看ServletInputStream可知,这个类并没有重写markSupported和reset方法。

综上,InputStream默认不实现reset方法,而ServletInputStream也没有重写reset相关方法,这样就无法重复读取流,这就是我们从request对象中输入流只能读取一次的原因。

三、解决方法

一般使用 HttpServletRequestWrapper + Filter 的方式处理,既然ServletInputStream不支持重新读写,那么我们就把流读出来后用容器存储起来,后面就可以多次利用了。

HttpServletRequestWrapper

它是一个http请求包装器,其基于装饰者模式实现了HttpServletRequest界面。

我们定义一个容器,将输入流里面的数据存储到这个容器里,然后我们重写getInputStream方法,每次都从这个容器里读数据,这样我们的输入流就可以读取任意次了。

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    //存储body数据的容器
    private byte[] cachedBody;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        //获取请求流
        InputStream requestInputStream = request.getInputStream();
        //以btye的方式,将其复制缓存起来
        this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedBodyServletInputStream(this.cachedBody);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
        return new BufferedReader(new InputStreamReader(byteArrayInputStream));
    }

}

CachedBodyServletInputStream

缓存输入流,继承 ServletInputStream。

public class CachedBodyServletInputStream extends ServletInputStream {

    private InputStream cachedBodyInputStream;

    public CachedBodyServletInputStream(byte[] cachedBody) {
        this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
    }

    @Override
    public int read() throws IOException {
        return cachedBodyInputStream.read();
    }

    @Override
    public boolean isFinished() {
        try {
            return cachedBodyInputStream.available() == 0;
        } catch (IOException e) {
            log.error("CachedBodyServletInputStream isFinished err: {}", e.getMessage(), e);
        }
        return false;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener readListener) {

    }
}

Filter处理

除了要写一个包装器外,我们还需要在过滤器里将原生的HttpServletRequest对象替换成我们的RequestWrapper对象。

除此以外,为了兼容文件上传的情况,我们还需要将其分开处理,对于multipart/form-data请求,我们使用spring原来的处理方式,不作特殊处理。

@WebFilter(filterName = "httpServletRequestWrapperFilter", urlPatterns = {"/*"})
public class BackendHttpServletRequestFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);

        try {

            HttpServletRequest httpRequest = (HttpServletRequest) request;

            String contentType = httpRequest.getContentType();
            //这里判断content-type,对于multipart/form-data类型将不作处理
            if (StrUtil.isNotBlank(contentType) && contentType.contains(ContentType.MULTIPART.getValue())) {//multipart/form-data类型
                //spring中使用MultipartResolver处理文件上传,所以这里需要将其封装往后传递
                MultipartResolver resolver = new StandardServletMultipartResolver();
                MultipartHttpServletRequest multipartRequest = resolver.resolveMultipart(httpRequest);

                chain.doFilter(multipartRequest, responseWrapper);

            } else {
                //对于其他的情况,我们统一使用包装类,将请求流进行缓存到容器
                CachedBodyHttpServletRequest cachedBodyHttpServletRequest =
                        new CachedBodyHttpServletRequest((HttpServletRequest) request);

                chain.doFilter(cachedBodyHttpServletRequest, responseWrapper);

            }

        } finally {
            //读取完 Response body 之后,通过这个设置回去,就可以使得接口调用者可以正常接收响应了,否则会产生空响应的情况
            //注意要在过滤器方法的最后调用
            responseWrapper.copyBodyToResponse();

        }
    }

}

至此,我们就可以实现拦截器的数据校验、aop日志、统一异常处理的邮件请求内容发送等功能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

reui

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值