场景重现
我需要使用过滤器的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 规范所决定的,以下是具体原因分析:
- 输入流的特性:HttpServletRequest 读取请求体数据主要是通过 getInputStream() 或getReader() 方法来获取输入流(ServletInputStream 或 BufferedReader)。以ServletInputStream 为例,它基于字节流,和普通的 InputStream类似,在读取数据时,数据会从底层的数据源(比如网络套接字)按照顺序读取到内存中。一旦数据被读取并从流中移除(比如通过调用 read()方法),就无法再从流的前面重新读取这些数据,因为流内部维护了一个读取位置指针,指针会随着数据的读取向后移动,到达流末尾后就没有更多数据可供再次读取了。
- 性能和资源管理的考量:Servlet容器设计时为了提高性能和资源管理效率,不会将请求体的数据全部缓存起来以便多次读取。如果每次读取请求体都要进行数据的缓存,那么在处理大量请求或者大请求体时,会占用大量的内存资源,降低服务器的性能和并发处理能力。因此,容器在处理请求时,更倾向于让开发者在一次机会中处理完请求体数据,而不是支持多次重复读取。
- 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 请求体数据的再次读取