怎样读取已经读取过的HttpServletRequest的请求体数据

场景重现

我需要使用过滤器的HttpServletRequest对http请求数据进行接收并做一些处理,过滤器后请求数据应该到达controller方法的形参中,但实际上访问controller方法的形参为null,所有为什么过滤器能获取到数据而controller方法获取不到。

原因

前置知识

在 Java Web 开发中,Filter(过滤器)是 Servlet 规范中非常重要的一个组件,它主要用于对客户端的请求和服务器的响应进行预处理和后处理。其中doFilter(ServletRequest request, ServletResponse response, FilterChain chain):这是 Filter 的核心方法,用于处理请求和响应。ServletRequest 和 ServletResponse 分别代表客户端的请求和服务器的响应。

HttpServletRequest:继承自 ServletRequest 接口,专门用于处理基于 HTTP 协议的请求。这意味着HttpServletRequest 不仅拥有 ServletRequest 的所有功能,还额外具备处理 HTTP 协议特定信息的方法。

原因分析

HttpServletRequest 中的请求体通常只能读取一次,这主要是由其底层实现机制和 Servlet 规范所决定的,以下是具体原因分析:

  1. 输入流的特性:HttpServletRequest 读取请求体数据主要是通过 getInputStream() 或getReader() 方法来获取输入流(ServletInputStream 或 BufferedReader)。以ServletInputStream 为例,它基于字节流,和普通的 InputStream类似,在读取数据时,数据会从底层的数据源(比如网络套接字)按照顺序读取到内存中。一旦数据被读取并从流中移除(比如通过调用 read()方法),就无法再从流的前面重新读取这些数据,因为流内部维护了一个读取位置指针,指针会随着数据的读取向后移动,到达流末尾后就没有更多数据可供再次读取了。
  2. 性能和资源管理的考量:Servlet容器设计时为了提高性能和资源管理效率,不会将请求体的数据全部缓存起来以便多次读取。如果每次读取请求体都要进行数据的缓存,那么在处理大量请求或者大请求体时,会占用大量的内存资源,降低服务器的性能和并发处理能力。因此,容器在处理请求时,更倾向于让开发者在一次机会中处理完请求体数据,而不是支持多次重复读取。
  3. Servlet 规范的规定:Servlet 规范中并没有强制要求容器实现请求体的可重复读取功能。在规范定义的
    HttpServletRequest 接口及其实现类的常规行为中,默认情况下请求体只能被读取一次。这就导致不同的 Servlet容器(如 Tomcat、Jetty 等)在实现时遵循这一原则,不支持请求体的重复读取。

解决

如果确实需要多次读取请求体数据,可以通过在第一次读取请求体时将数据缓存到内存(对于较小的请求体)或临时文件(对于较大的请求体)中,然后后续从缓存中读取数据。在 Spring 框架中,可以通过自定义过滤器,实现 HttpServletRequestWrapper 类,在过滤器中读取并缓存请求体,从而实现多次读取的效果。

  • 使用 HttpServletRequestWrapper 自定义包装类
    HttpServletRequestWrapper 是 Servlet 提供的一个包装类,可用于扩展 HttpServletRequest 的功能。通过自定义包装类,在首次读取请求体时将数据缓存起来,后续就可以从缓存中再次读取。

    示例代码

    import javax.servlet.ReadListener;
    import javax.servlet.ServletInputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletRequestWrapper;
    import java.io.*;
    
    public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
    
        private final byte[] cachedBody;
    
        public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
            super(request);
            // 读取请求体数据并缓存
            InputStream requestInputStream = request.getInputStream();
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = requestInputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, bytesRead);
            }
            this.cachedBody = byteArrayOutputStream.toByteArray();
        }
    
        @Override
        public ServletInputStream getInputStream() throws IOException {
            return new CachedBodyServletInputStream(this.cachedBody);
        }
    
        @Override
        public BufferedReader getReader() throws IOException {
            ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
            InputStreamReader inputStreamReader = new InputStreamReader(byteArrayInputStream);
            return new BufferedReader(inputStreamReader);
        }
    
        private static class CachedBodyServletInputStream extends ServletInputStream {
    
            private final ByteArrayInputStream byteArrayInputStream;
    
            public CachedBodyServletInputStream(byte[] cachedBody) {
                this.byteArrayInputStream = new ByteArrayInputStream(cachedBody);
            }
    
            @Override
            public boolean isFinished() {
                return byteArrayInputStream.available() == 0;
            }
    
            @Override
            public boolean isReady() {
                return true;
            }
    
            @Override
            public void setReadListener(ReadListener readListener) {
                throw new UnsupportedOperationException();
            }
    
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        }
    }
    

    使用示例

    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    
    @WebFilter(urlPatterns = "/*")
    public class RequestCachingFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            if (request instanceof HttpServletRequest) {
                HttpServletRequest httpServletRequest = (HttpServletRequest) request;
                // 包装原始请求
                CachedBodyHttpServletRequest cachedRequest = new CachedBodyHttpServletRequest(httpServletRequest);
                // 传递包装后的请求到后续过滤器和 Servlet
                chain.doFilter(cachedRequest, response);
            } else {
                chain.doFilter(request, response);
            }
        }
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            // 初始化操作
        }
    
        @Override
        public void destroy() {
            // 销毁操作
        }
    }
    
  • 在 Spring 框架中使用 ContentCachingRequestWrapper
    Spring 框架提供了 ContentCachingRequestWrapper 类,可用于缓存请求体数据,方便后续多次读取。

    示例代码

    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    public class RequestCachingSpringFilter extends OncePerRequestFilter {
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            // 包装请求
            org.springframework.web.util.ContentCachingRequestWrapper wrappedRequest = new org.springframework.web.util.ContentCachingRequestWrapper(request);
            filterChain.doFilter(wrappedRequest, response);
        }
    }
    

    配置过滤器

    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class FilterConfig {
    
        @Bean
        public FilterRegistrationBean<RequestCachingSpringFilter> loggingFilter() {
            FilterRegistrationBean<RequestCachingSpringFilter> registrationBean = new FilterRegistrationBean<>();
            registrationBean.setFilter(new RequestCachingSpringFilter());
            registrationBean.addUrlPatterns("/*");
            return registrationBean;
        }
    }
    

说明

  • 自定义包装类方式:适用于纯 Servlet 环境,通过自定义 HttpServletRequestWrapper
    类,手动实现请求体数据的缓存和再次读取。

  • Spring 框架方式:在 Spring 项目中,使用 ContentCachingRequestWrapper
    可以更方便地实现请求体数据的缓存,减少手动编码的工作量。

通过以上方法,就可以实现对已经读取过的 HttpServletRequest 请求体数据的再次读取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值