java.lang.IllegalStateException: getReader()已经为此请求调用过。

45

我想在我的Servlet中添加日志记录,因此我创建了一个过滤器(Filter),它应该显示请求并跳转到Servlet。但不幸的是,我遇到了异常:

java.lang.IllegalStateException: getReader() has already been called for this request
    at org.apache.catalina.connector.Request.getInputStream(Request.java:948)
    at org.apache.catalina.connector.RequestFacade.getInputStream(RequestFacade.java:338)
    at com.noelios.restlet.ext.servlet.ServletCall.getRequestEntityStream(ServletCall.java:190)

所以为了解决这个问题,我找到了使用Wrapper的解决方案,但它并不起作用。在代码中还能使用/更改什么呢?有什么想法吗?

[MyHttpServletRequestWrapper]

public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper
{
    public MyHttpServletRequestWrapper(HttpServletRequest request)
    {
        super(request);
    }

    private String getBodyAsString()
    {
        StringBuffer buff = new StringBuffer();
        buff.append(" BODY_DATA START [ ");
        char[] charArr = new char[getContentLength()];
        try
        {
            BufferedReader reader = new BufferedReader(getReader());
            reader.read(charArr, 0, charArr.length);
            reader.close();
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        buff.append(charArr);
        buff.append(" ] BODY_DATA END ");
        return buff.toString();
    }

    public String toString()
    {
        return getBodyAsString();
    }
}

[我的过滤器]

public class MyFilterimplements Filter
{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
    {
        final HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        final HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        final HttpServletRequestWrapper requestWrapper = new MyHttpServletRequestWrapper(httpServletRequest);
        final String requestBody = requestWrapper.toString();

        chain.doFilter(request, response);
    }
}
6个回答

32

看起来 Restlet 框架在 Request 对象上调用了 getRequestEntityStream(),而这个方法又调用了 getInputStream(),因此调用请求的 getReader() 会抛出 IllegalStateException 异常。Servlet API 文档对于 getReader()getInputStream() 的说明如下:

 public java.io.BufferedReader getReader()
    ...
    ...
Throws:
    java.lang.IllegalStateException - if getInputStream() method has been called on this request

 public ServletInputStream getInputStream()
    ...
    ...
    Throws:
    java.lang.IllegalStateException - if the getReader() method has already been called for this request

从文档上看,似乎我们不能同时在请求对象上调用getReader()getInputStream()。我建议你在你的包装器中使用getInputStream()而不是getReader()

17
使用ContentCachingRequestWrapper类。将HttpServletRequest包装在其中可以解决这个问题。 示例:如果你想要转换你的"HttpServletRequest servletRequest",你可以做一些类似的事情。
import org.springframework.web.util.ContentCachingRequestWrapper;

ContentCachingRequestWrapper request = new ContentCachingRequestWrapper(servletRequest);

希望能有所帮助!

8
据我所知,Servlet在这方面基本上是有缺陷的。您可以尝试按照这里所述的方法解决此问题,但这会在其他尝试与其协作时引起其他神秘问题。
实际上,他建议克隆请求、读取主体,然后在克隆类中覆盖getReader和getInputStream方法以返回已检索到的内容。
最终我得到了以下代码:
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

//this class stops reading the request payload twice causing an exception
public class WrappedRequest extends HttpServletRequestWrapper
{
    private String _body;
    private HttpServletRequest _request;

    public WrappedRequest(HttpServletRequest request) throws IOException
    {
        super(request);
        _request = request;

        _body = "";
        try (BufferedReader bufferedReader = request.getReader())
        {
            String line;
            while ((line = bufferedReader.readLine()) != null)
                _body += line;
        }
    }

    @Override
    public ServletInputStream getInputStream() throws IOException
    {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes());
        return new ServletInputStream()
        {
            public int read() throws IOException
            {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() throws IOException
    {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}

然而,一切似乎都很顺利,直到我们意识到从浏览器上传文件不起作用。我通过二分法找出变化的原因。

那篇文章中有些人说,你需要重写与参数相关的方法,但没有解释如何做到这一点。

因此,我检查了两个请求之间是否有任何差异。然而,在克隆请求后,它们都具有相同的参数集(原始请求+克隆请求均无参数),以及相同的标头集。

但是,在某种程度上,请求正在受到影响,并且在下游进一步破坏了对请求的理解 - 在我的情况下,它导致库(extdirectspring)中的一个奇怪错误,其中某个东西试图将内容读取为Json。将过滤器中读取正文的代码删除后,它可以再次工作。

我的调用代码如下:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException
{
    HttpServletRequest properRequest = ((HttpServletRequest)request);

    String pathInfo = properRequest.getPathInfo();
    String target = "";
    if(pathInfo == null)
        pathInfo = "";

    if(pathInfo.equals("/router"))
    {
        //note this is because servlet requests hate you!
        //if you read their contents more than once then they throw an exception so we need to do some madness
        //to make this not the case
        WrappedRequest wrappedRequest = new WrappedRequest(properRequest);
        target = ParseExtDirectTargetFrom(wrappedRequest);
        request = wrappedRequest;
    }

    boolean callingSpecialResetMethod = pathInfo.equals("/resetErrorState") || target.equals("resetErrorState");
    if(_errorHandler.IsRejectingRequests() && !callingSpecialResetMethod)
        return;

    try {
        filterChain.doFilter(request, response);
    }
    catch (Exception exception) {
        ((HttpServletResponse) response).sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "ERROR");
        _errorHandler.NotifyOf(exception);
    }
}

我省略了ParseExtDirectTargetFrom的内容,但它调用了getReader()。
在我的情况下,该过滤器适用于所有其他请求,但是这种奇怪的行为使我意识到某些事情不太对劲,并且我尝试做的事情(为测试实现合理的异常处理行为)不值得潜在地破坏随机的未来请求(因为我无法弄清楚是什么导致请求变得不稳定)。
此外,值得注意的是,损坏的代码是不可避免的-我假设它可能是spring的某个东西,但是ServletRequest一直往上走-即使您通过从HttpServlet子类化来制作servlet,您也只会得到这个结果。
我的建议是-不要在过滤器中读取请求正文。这将打开一个会导致以后出现奇怪问题的问题。

6
主要的问题在于无法同时将输入读取为二进制流和字符流,即使一个在过滤器中,另一个在servlet中也是如此。

2
使用其他解决方案可能会导致以下异常。
java.lang.IllegalStateException: getInputStream() has already been called for this request

要读取HttpServletRequest,需要实现以下内容。

背景:

要从请求中获取请求体,HttpServletRequest提供了InputStream类。getReader()通常用于流式传输请求。该函数在内部调用getInputStream()函数,该函数返回一个流,供我们读取请求。现在,请注意它是一个流,只能打开一次。因此,在读取时(即实现本主题中给出的解决方案时),它通常会抛出“流已关闭”的异常。这是因为Tomcat服务器已经打开并读取了请求一次。因此,我们需要在这里实现一个包装器,通过保留其实例来帮助我们重新打开已读取的流。这个包装器也不能直接使用,而是需要在Spring过滤器级别上添加,在Tomcat服务器正在读取时。

代码:

Servlet请求包装器类:

import lombok.extern.slf4j.Slf4j;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
@Slf4j
public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final String body;public MyHttpServletRequestWrapper(HttpServletRequest request) {
    super(request);

    StringBuilder stringBuilder = new StringBuilder();
    BufferedReader bufferedReader = null;

    try {
        InputStream inputStream = request.getInputStream();

        if (inputStream != null) {
            bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

            char[] charBuffer = new char[128];
            int bytesRead = -1;

            while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                stringBuilder.append(charBuffer, 0, bytesRead);
            }
        } else {
            stringBuilder.append("");
        }
    } catch (IOException ex) {
        log.error("Error reading the request body...");
    } finally {
        if (bufferedReader != null) {
            try {
                bufferedReader.close();
            } catch (IOException ex) {
                log.error("Error closing bufferedReader...");
            }
        }
    }

    body = stringBuilder.toString();
}

@Override
public ServletInputStream getInputStream () {
    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());

    ServletInputStream inputStream = new ServletInputStream() {
        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }

        public int read () throws IOException {
            return byteArrayInputStream.read();
        }
    };

    return inputStream;
}

@Override
public BufferedReader getReader(){
    return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}

现在我们需要使用包装器,通过在过滤器中实现它,如下所示。
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Objects;
@Component
@Order(1)
@Slf4j
public class ServletFilter implements Filter {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if (servletRequest instanceof HttpServletRequest) {
            requestWrapper = new MyHttpServletRequestWrapper((HttpServletRequest) servletRequest);
        }
        if (Objects.isNull(requestWrapper)){
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
}
}

然后,可以按照以下建议的实施方式来获取请求体如下:

    private String getRequestBody(HttpServletRequest request) {
    try {
        return request.getReader().lines().collect(Collectors.joining());
    }catch (Exception e){
        e.printStackTrace();
        return "{}";
    }
}

0
嗯,也许这是一件很明显的事情,但我想与您分享这段对我来说运行良好的代码。在一个使用JWT的Spring Boot项目中,对于客户端的请求,需要将所有请求及其响应保存在数据库表中,并同时授权访问资源。当然,我使用了getReader()来获取请求体,但是我得到了java.lang.IllegalStateException...
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomAuthorizationFilter extends OncePerRequestFilter {

private final AuthorizationService authorizationService;
private String requestBody;     

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
    HttpRequestDto requestDto = new HttpRequestDto();
    try {
        if (RequestMethod.POST.name().equalsIgnoreCase(request.getMethod()) && requestBody != null) { //This line and validation is useful for me [requestBody != null]
            requestBody = request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
        }
        //Do all JWT control
        requestDto.setRequestURI(request.getRequestURI());
        requestDto.setMethod(request.getMethod());
        requestDto.setBody(requestBody);
    }catch (IOException ie) {
        responseError(_3001, response, ie);
    } finally {
        try {
            ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
            filterChain.doFilter(request, responseWrapper);
            saveResponse(responseWrapper, requestDto);
        } catch (ServletException | IOException se) {
            responseError(_3002, response, se);
        }
    }
}


private void saveResponse(ContentCachingResponseWrapper responseWrapper, HttpRequestDto requestDto) {
    try {
        HttpResponseDto responseDto = new HttpResponseDto();
        responseDto.setStatus(responseWrapper.getStatus());
        byte[] responseArray = responseWrapper.getContentAsByteArray();
        String responseBody = new String(responseArray, responseWrapper.getCharacterEncoding());
        responseDto.setBody(responseBody);
        responseWrapper.copyBodyToResponse();
        authorizationService.seveInDatabase(requestDto, responseDto);            
    } catch (Exception e) {
        log.error("Error ServletException | IOException in CustomAuthorizationFilter.saveResponse", e);
    }
}

private void responseError(LogCode code, HttpServletResponse response, Exception e) {
    try {
        Map<String, Object> error = new HashMap<>();
        error.put("log", LogUtil.getLog(code));
        error.put("message", e.getMessage());
        response.setContentType(APPLICATION_JSON_VALUE);
        new ObjectMapper().writeValue(response.getOutputStream(), error);
        e.printStackTrace();
    } catch (IOException ie) {
        log.error("Error IOException in HttpLoggingFilter.responseError:", ie);
    }
}


public String getRequestBody() {
    return requestBody;
}

public void setRequestBody(String requestBody) {
    this.requestBody = requestBody;
}

}

所以我的解决方案是使用本地属性requestBody的getter和setter方法,以验证它是否为空,并且不再调用getReader()方法,因为在设置值时已经保存在内存中。这对我来说非常完美。祝好。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接