1. 引言

在Java Web应用中,当我们尝试在ServletRequest接口上调用getReader()方法时,有时可能会遇到IllegalStateException异常,其错误信息为:“此请求已调用了getInputStream()”。本文将解释为什么会发生这种情况,并提供解决方案。

2. 问题与原因

Java Servlet规范为构建Web应用提供了基础,它定义了ServletRequestHttpServletRequest接口,以及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上找到。