Java IllegalStateException: “getInputStream() has already been called for this request”

1、介绍

有时,当我们在 Java Web 应用程序中调用 ServletRequest 接口的 getReader() / getInputStream()方法时,可能会出现IllegalStateException 异常,异常信息为:“getInputStream() has already been called for this request”。

在本教程中,我们将了解出现这种异常的原因和解决方法。

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

那么我们如何在应用程序中避免此类异常呢?一个简单的方法就是避免同时调用它们。但有些 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、总结

在本文中,我们查看了 ServletRequest 的文档,了解了为什么会出现 IllegalStateException。然后,我们学习了使用 Spring MVC 框架提供的 ContentCachingRequestWrapper 的解决方案。


参考:https://www.baeldung.com/java-servletrequest-illegalstateexception