Java IllegalStateException:“已經為此請求調用了 getInputStream()”
一、簡介
有時,當我們在 Java Web 應用程序中調用ServletRequest
接口的getReader()
方法時,我們可能會得到IllegalStateException
異常,錯誤消息“getInputStream() has already been called for this request”
。
在本教程中,我們將了解為什麼會發生這種情況以及如何解決它。
二、問題及原因
Java Servlet 規範用於在 Java 中構建 Web 應用程序。它使用方法getReader()
和getInputStream()
定義接口ServletRequest
/ HttpServletRequest
以從 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()
時,我們會得到IllegalStateException
: “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
那麼在我們的應用中如何避免此類異常呢?一種簡單的方法是避免同時調用它們。但是一些網絡框架可能會在我們的代碼之前從請求中讀取數據。如果我們想要多次檢查輸入流,使用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
的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 上找到。