1. 引言
在Java Web应用中,当我们尝试在ServletRequest
接口上调用getReader()
方法时,有时可能会遇到IllegalStateException
异常,其错误信息为:“此请求已调用了getInputStream()”。本文将解释为什么会发生这种情况,并提供解决方案。
2. 问题与原因
Java Servlet规范为构建Web应用提供了基础,它定义了ServletRequest
和HttpServletRequest
接口,以及getReader()
和getInputStream()
方法,用于从HTTP请求中读取数据。
getReader()
方法获取请求体作为字符数据,而getInputStream()
则获取请求体作为二进制数据。getReader()
和getInputStream()
的方法文档强调两者不能同时使用:
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服务器容器中,如果我们在调用getReader()
后又调用getInputStream()
,就会得到“此请求已调用了getInputStream()”的异常。反之亦然。
以下是一个重现此情况的测试示例:
@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
如何在应用中避免此类异常呢?一个简单的方法是避免同时调用它们。但有些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
遵循装饰器模式,它重写了getInputStream()
和getReader()
方法,以防止抛出IllegalStateException
。它还定义了一个ContentCachingInputStream
,用于包装原始的ServletInputStream
,并将数据缓存到输出流中。
读取完Request
对象的数据后,ContentCachingInputStream
会帮助我们将字节缓存到ByteArrayOutputStream
类型的cachedContent
对象中。然后,我们可以通过调用getContentAsByteArray()
方法重复读取数据。
在使用ContentCachingRequestWrapper
之前,我们需要创建一个过滤器,将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);
// now we can call both getInputStream() and getReader()
request.getInputStream();
request.getReader();
}
实际上,ContentCachingRequestWrapper
有一个限制,即不能多次读取请求。尽管我们采用了ContentCachingRequestWrapper
,但我们仍然从请求对象的ServletInputStream
中读取字节。然而,默认的ServletInputStream
实例不支持多次读取。当到达流的末尾时,调用ServletInputStream.read()
始终返回-1
。
若要克服这个限制,我们需要自定义实现ServletRequest
。
4. 总结
在这篇文章中,我们查阅了ServletRequest
的文档,理解了为何会出现IllegalStateException
。然后,我们学习了如何使用Spring MVC框架提供的ContentCachingRequestWrapper
来解决问题。
如往常一样,这里的所有代码片段可以在GitHub上找到。