ServletRequest一旦读取了流,流就关闭了,流中的数据一旦被消费就不能再次从流中读取了,Servlet 是如何还能够从ServletRequest流中读取数据的
是的,在默认情况下,一旦 ServletRequest
的输入流(InputStream
或 Reader
)被读取,流就被标记为已消费,数据也无法再次读取。这是因为 ServletRequest
的输入流基于 HTTP 请求的字节流实现,读取数据后,流会关闭或标记为已消费状态,从而阻止重复读取。
如何解决无法重复读取流的问题
在某些情况下,我们可能需要在多个地方处理请求数据(如身份验证、日志记录或业务逻辑等),那么可以通过以下几种方式来解决流不能重复读取的问题:
1. 使用 HttpServletRequestWrapper
缓存请求数据
可以创建一个自定义的 HttpServletRequestWrapper
,在请求第一次被读取时缓存流中的数据,以便后续可以多次读取。
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = requestInputStream.readAllBytes();
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(cachedBody);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private static class CachedBodyServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.buffer = new ByteArrayInputStream(cachedBody);
}
@Override
public int read() {
return buffer.read();
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
}
}
在这个自定义的 HttpServletRequestWrapper
中,流第一次被读取后缓存到 cachedBody
字节数组,后续可以多次读取。使用时,在 Filter
或 Servlet
中用包装后的 HttpServletRequest
替换原始请求对象。
2. 在过滤器中替换 HttpServletRequest
可以在过滤器中将原始请求替换为 CachedBodyHttpServletRequest
实例,以便后续在 Servlet
中可以重复读取流。
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class RequestBodyCachingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest cachedRequest = new CachedBodyHttpServletRequest((HttpServletRequest) request);
chain.doFilter(cachedRequest, response);
} else {
chain.doFilter(request, response);
}
}
}
总结
- 默认情况下,
ServletRequest
的输入流读取后便不可再读。 - 可以使用
HttpServletRequestWrapper
来缓存请求体内容。 - 使用缓存后的请求对象即可实现流的重复读取。
在 Java Web 应用中,Filter
中读取 HttpServletRequest
的输入流通常会导致流被“消费”,从而在后续的 Servlet
中无法再次读取。为了使 HttpServletRequest
在 Filter
和 Servlet
中都能够读取,通常使用缓存技术,即通过将请求数据存储在内存中来实现流的重复读取。这是因为 HttpServletRequest
默认的流是一次性读取的,原始数据被消费后便不能再次获取。
如果在 Filter
中读取 HttpServletRequest
后在 Servlet
中仍然可以读取流,通常是以下几种情况之一:
1. 使用了包装类(HttpServletRequestWrapper
)缓存请求数据
这是最常用的解决方案。通过自定义一个 HttpServletRequestWrapper
,在流被读取时,将数据缓存为字节数组。这样一来,每次请求读取时都可以从缓存中返回新的流,避免了流被“消费”后无法再次读取的问题。
实现步骤大致如下:
- 在
Filter
中创建包装类的实例。 - 缓存请求体,并将包装类传递到后续的
FilterChain
中。 - 在
Servlet
中,包装类的getInputStream()
或getReader()
方法会返回新的流对象,从而实现流的重复读取。
示例包装类代码见前面的回答。
2. 使用了 Spring 框架
在 Spring MVC 中,RequestBody
也可以通过 Filter
和 Servlet
重复读取。Spring 内部使用了 ContentCachingRequestWrapper
来包装 HttpServletRequest
,其中的请求体被缓存到字节数组,以实现流的重复读取。例如,Spring 的 OncePerRequestFilter
和其他一些过滤器会自动包装 HttpServletRequest
。
要启用 ContentCachingRequestWrapper
,可以在项目中引入 Spring MVC,然后在 Filter
中手动创建或让 Spring 自动管理。例如:
import org.springframework.web.util.ContentCachingRequestWrapper;
public class MyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response);
// 读取请求体
byte[] requestBody = wrappedRequest.getContentAsByteArray();
// 进一步处理 requestBody
}
}
3. 使用支持多次读取的 Servlet 容器
某些 Servlet 容器在实现时支持多次读取请求流,但这是容器实现的特性,通常不能依赖容器支持来解决该问题。例如,某些定制化的 Servlet 容器会自动缓存请求体,但这不适用于所有环境,也不适用于所有容器。
总结
- 默认情况下,读取
HttpServletRequest
的输入流是一次性的。 - 可以通过
HttpServletRequestWrapper
来缓存请求体,从而在Filter
和Servlet
中重复读取流。 - Spring MVC 的
ContentCachingRequestWrapper
是实现流重复读取的常用方案。