1、介绍 {#1介绍}
有时,当我们在 Java Web 应用程序中调用 ServletRequest
接口的 getReader()
/ getInputStream()
方法时,可能会出现IllegalStateException
异常,异常信息为:"getInputStream() has already been called for this request"。
在本教程中,我们将了解出现这种异常的原因和解决方法。
2、问题与原因 {#2问题与原因}
Java Servlet 规范,用于用 Java 构建 Web 应用程序。它定义了 ServletRequest
/ HttpServletRequest
接口,以及 getReader()
和 getInputStream()
方法,用于从 HTTP 请求中读取数据。
getReader()
方法以字符数据形式返回请求体,而 getInputStream()
方法则以二进制数据形式返回请求体。
getReader()
和 getInputStream()
的 Servlet API 文档强调,它们不能同时使用:
public java.io.BufferedReader getReader()
Either this method or getInputStream may be called to read the body, not both.
...
Throws:
java.lang.IllegalStateException - if getInputStream() method has been called on this request
public ServletInputStream getInputStream()
Either this method or getReader may be called to read the body, not both.
...
Throws:
java.lang.IllegalStateException - if the getReader() method has already been called for this request
因此,在使用 Tomcat 等 servlet 容器时,当我们在 getInputStream()
之后调用 getReader()
时,我们会收到异常 IllegalStateException
:"getInputStream() has already been called for this request"。当我们在 getReader()
之后调用 getInputStream()
时,我们会收到:"getReader() has already been called for this request"。
下面是一个重现这种情况的测试:
@Test
void shouldThrowIllegalStateExceptionWhenCalling_getReaderAfter_getInputStream() throws IOException {
HttpServletRequest request = new MockHttpServletRequest();
try (ServletInputStream ignored = request.getInputStream()) {
IllegalStateException exception = assertThrows(IllegalStateException.class, request::getReader);
assertEquals("Cannot call getReader() after getInputStream() has already been called for the current request",
exception.getMessage());
}
}
我们使用 MockHttpServletRequest
来模拟这种情况。如果我们在 getReader()
之后调用 getInputStream()
,也会得到类似的错误信息。在不同的实现中,错误信息可能会略有不同。
3、使用 ContentCachingRequestWrapper 避免 IllegalStateException {#3使用-contentcachingrequestwrapper-避免-illegalstateexception}
那么我们如何在应用程序中避免此类异常呢?一个简单的方法就是避免同时调用它们。但有些 web 框架可能会在我们的代码之前读取请求体中的数据。如果我们想多次读取输入流,可以使用 Spring MVC 框架提供的 ContentCachingRequestWrapper
。
让我们看看 ContentCachingRequestWrapper
的核心部分:
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
private final ByteArrayOutputStream cachedContent;
//....
@Override
public ServletInputStream getInputStream() throws IOException {
if (this.inputStream == null) {
this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
}
return this.inputStream;
}
@Override
public BufferedReader getReader() throws IOException {
if (this.reader == null) {
this.reader = new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
}
return this.reader;
}
public byte[] getContentAsByteArray() {
return this.cachedContent.toByteArray();
}
//....
}
ContentCachingRequestWrapper
按照装饰器模式封装 ServletRequest
对象。它重写了 getInputStream()
和 getReader()
方法,以避免抛出 IllegalStateException
。它还定义了一个 ContentCachingInputStream
来封装原始 ServletInputStream
,以便将数据缓存到输出流中。
从 Request
对象读取数据后,ContentCachingInputStream
会帮助我们将字节缓存到 ByteArrayOutputStream
类型的缓存内容对象中。然后,我们可以通过调用其 getContentAsByteArray()
方法重复读取数据。
在使用 ContentCachingRequestWrapper
之前,我们需要创建一个 filter,将 ServletRequest
转换为 ContentCachingRequestWrapper
:
@WebFilter(urlPatterns = "/*")
public class CacheRequestContentFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
if (request instanceof HttpServletRequest) {
String contentType = request.getContentType();
if (contentType == null || !contentType.contains("multipart/form-data")) {
request = new ContentCachingRequestWrapper((HttpServletRequest) request);
}
}
chain.doFilter(request, response);
}
}
最后,我们创建一个测试,以确保它能按预期运行:
@Test
void givenServletRequest_whenDoFilter_thenCanCallBoth() throws ServletException, IOException {
MockHttpServletRequest req = new MockHttpServletRequest();
MockHttpServletResponse res = new MockHttpServletResponse();
MockFilterChain chain = new MockFilterChain();
Filter filter = new CacheRequestContentFilter();
filter.doFilter(req, res, chain);
ServletRequest request = chain.getRequest();
assertTrue(request instanceof ContentCachingRequestWrapper);
// 现在我们可以同时调用 getInputStream() 和 getReader()
request.getInputStream();
request.getReader();
}
实际上,ContentCachingRequestWrapper
有一个限制,即我们不能多次读取 request。虽然我们采用了 ContentCachingRequestWrapper
,但我们仍然从 request 对象的 ServletInputStream
中读取字节。默认情况下 ServletInputStream
实例不支持多次读取数据。当我们读到数据流的末尾时,调用 ServletInputStream.read()
将始终返回 -1
。
如果要克服这一限制,我们需要自己实现 ServletRequest
。
4、总结 {#4总结}
在本文中,我们查看了 ServletRequest
的文档,了解了为什么会出现 IllegalStateException
。然后,我们学习了使用 Spring MVC 框架提供的 ContentCachingRequestWrapper
的解决方案。
参考:https://www.baeldung.com/java-servletrequest-illegalstateexception