欢迎来到信息岛!
adminAdmin  2024-10-02 04:54 信息岛 显示边栏 |   抢沙发  28 
文章评分 0 次,平均分 0.0

对Request请求对象为InputStream流时进行可重复读过滤

filterrepeatableread.jpg)

背景

在处理 HttpServletRequest 时,InputStream 只能读取一次,这是因为流的设计是单次消费的,也就是说一旦读取后,流的数据就不能再被读取。

这在某些情况下会导致问题,例如在过滤器中读取了请求流之后,控制器尝试再次读取时会抛出错误。

示例:登录功能在过滤器中读取流进行校验验证码,再到Controller中读取流进行业务处理,这时Controller就无法进入,因为读取不到流了。

要解决这个问题,可以使用下面两种方案:

前提

关于SpringBoot中五种过滤器用法和区别,比如下面会用到OncePerRequestFilter抽象类和Filter接口,

可以查看这篇文章的详细解读:直达地址

方案一:使用 ContentCachingRequestWrapper

ContentCachingRequestWrapper 是 Spring 提供的一个包装类,它可以在读取时缓存请求的内容,因此可以重复读取 InputStream。具体步骤如下:

示例

1、在过滤器中使用 ContentCachingRequestWrapper

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

在新版的Spring中ContentCachingRequestWrapper中使用FastByteArrayOutputStream来取代ContentCachingRequestWrapper,不需要初始化ContentCachingRequestWrapper的时候就申请大块内存,lazy化的。

GitHub源码更新说明

方案二:自定义 HttpServletRequestWrapper

如果不想依赖 Spring 的 ContentCachingRequestWrapper,可以自定义一个继承 HttpServletRequestWrapper 的类,将请求体缓存到内存中,以支持重复读取。

示例

1、定义自定义请求包装类

import cn.hutool.core.io.IoUtil;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

/**
 * 缓存body参数的请求对象,即可重复调用request.getInputStream(),解决流只能读取一次的问题。
 * @author PanXun
 */
public class CacheBodyRequest extends HttpServletRequestWrapper {

    //请求体
    private final byte[] requestBody;

    public CacheBodyRequest(HttpServletRequest request) throws IOException {
        super(request);
        this.requestBody = IoUtil.readBytes(request.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(requestBody);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return inputStream.available() == 0;
            }

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

            @Override
            public void setReadListener(ReadListener listener) {
            }

            @Override
            public int read() {
                return inputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

2、在过滤器中使用自定义包装类

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 将 默认的请求对象 替换为 缓存body的请求对象,即 HttpServletRequest -> CacheBodyRequest
 * @author PanXun
 */
public class ReplaceHttpRequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

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

        // 替换请求对象 HttpServletRequest -> CacheBodyRequest
        CacheBodyRequest cacheBodyRequest = new CacheBodyRequest(httpServletRequest);

        chain.doFilter(cacheBodyRequest, response);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

方案二:改进

上面说到在新版的 Spring 中,ContentCachingRequestWrapper 使用 FastByteArrayOutputStream 来提高效率和性能。FastByteArrayOutputStream 是一个比传统的 ByteArrayOutputStream 更高效的实现,能够减少内存的拷贝次数,从而提升性能。

那么对方案二也可以做出同样的优化,优化后的代码如下:

示例

1、自定义请求包装类

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.springframework.util.FastByteArrayOutputStream;

/**
 * 缓存body参数的请求对象,即可重复调用request.getInputStream(),解决流只能读取一次的问题。
 * @author PanXun
 */
public class CacheBodyRequest extends HttpServletRequestWrapper {

    // 缓存请求体的字节数组
    private final byte[] requestBody;

    public CacheBodyRequest(HttpServletRequest request) throws IOException {
        super(request);
        // 使用 FastByteArrayOutputStream 来缓存请求体
        InputStream requestInputStream = request.getInputStream();
        this.requestBody = inputStreamToByteArray(requestInputStream);
    }

    // 使用 FastByteArrayOutputStream 替代 ByteArrayOutputStream
    private byte[] inputStreamToByteArray(InputStream inputStream) throws IOException {
        FastByteArrayOutputStream fastByteArrayOutputStream = new FastByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int len;
        while ((len = inputStream.read(buffer)) != -1) {
            fastByteArrayOutputStream.write(buffer, 0, len);
        }
        return fastByteArrayOutputStream.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(requestBody);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }

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

            @Override
            public void setReadListener(ReadListener listener) {
            }

            @Override
            public int read() {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

2、在过滤器中使用自定义的包装类

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 将 默认的请求对象 替换为 缓存body的请求对象,即 HttpServletRequest -> CacheBodyRequest
 * @author PanXun
 */
public class ReplaceHttpRequestFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

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

        // 替换请求对象 HttpServletRequest -> CacheBodyRequest
        CacheBodyRequest cacheBodyRequest = new CacheBodyRequest(httpServletRequest);

       // 放行
        chain.doFilter(cacheBodyRequest, response);
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

结论

使用 ContentCachingRequestWrapper 是最方便的方案,它已经内置于 Spring 框架中,适合与 Spring 框架结合使用。如果不想引入 Spring 特定的类,可以通过自定义 HttpServletRequestWrapper 实现重复读取流。

「点点赞赏,手留余香」

还没有人赞赏,快来当第一个赞赏的人吧!

admin给Admin打赏
×
予人玫瑰,手有余香
  • 2
  • 5
  • 10
  • 20
  • 50
2
支付

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

admin
Admin 关注:0    粉丝:0 最后编辑于:2024-10-08
这个人很懒,什么都没写

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享