如何多次读取 request.getInputStream()

Posted

技术标签:

【中文标题】如何多次读取 request.getInputStream()【英文标题】:How to read request.getInputStream() multiple times 【发布时间】:2011-05-25 20:19:57 【问题描述】:

我有这个代码:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException 
    logger.info("Filter start...");

    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    String ba = getBaId(getBody(httpRequest));

    if (ba == null) 
        logger.error("Wrong XML");
        httpResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST);
     else       

        if (!clients.containsKey(ba)) 
            clients.put(ba, 1);
            logger.info("Client map : init...");
         else 
            clients.put(ba, clients.get(ba).intValue() + 1);
            logger.info("Threads for " + ba + " = " + clients.get(ba).toString());
        

        chain.doFilter(request, response);
    

还有这个 web.xml(包被缩短了,名字也变了,但是看起来一样)

<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app>
  <filter>
    <filter-name>TestFilter</filter-name>
    <filter-class>pkg.TestFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>TestFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>WEB-INF/applicationContext.xml</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>Name</servlet-name>
    <display-name>Name</display-name>
    <servlet-class>pkg.Name</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Name</servlet-name>
    <url-pattern>/services/*</url-pattern>
  </servlet-mapping>
</web-app>

我想在过滤器之后调用 Servlet。我希望chain.doFilter(...) 能做到这一点,但我总是在chain.doFilter(...) 上得到这个错误:

java.lang.IllegalStateException: getInputStream() can't be called after getReader()
at com.caucho.server.connection.AbstractHttpRequest.getInputStream(AbstractHttpRequest.java:1933)
at org.apache.cxf.transport.http.AbstractHTTPDestination.setupMessage(AbstractHTTPDestination.java:249)
at org.apache.cxf.transport.servlet.ServletDestination.invoke(ServletDestination.java:82)
at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:283)
at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:166)
at org.apache.cxf.transport.servlet.AbstractCXFServlet.invoke(AbstractCXFServlet.java:174)
at org.apache.cxf.transport.servlet.AbstractCXFServlet.doPost(AbstractCXFServlet.java:152)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:153)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:91)
at com.caucho.server.dispatch.ServletFilterChain.doFilter(ServletFilterChain.java:103)
at pkg.TestFilter.doFilter(TestFilter.java:102)
at com.caucho.server.dispatch.FilterFilterChain.doFilter(FilterFilterChain.java:87)
at com.caucho.server.webapp.WebAppFilterChain.doFilter(WebAppFilterChain.java:187)
at com.caucho.server.dispatch.ServletInvocation.service(ServletInvocation.java:265)
at com.caucho.server.http.HttpRequest.handleRequest(HttpRequest.java:273)
at com.caucho.server.port.TcpConnection.run(TcpConnection.java:682)
at com.caucho.util.ThreadPool$Item.runTasks(ThreadPool.java:743)
at com.caucho.util.ThreadPool$Item.run(ThreadPool.java:662)
at java.lang.Thread.run(Thread.java:619)

【问题讨论】:

是的,它应该可以工作。 servlet 是否可以在没有过滤器的情况下工作? 没有过滤器的 servlet 工作,没有chain.doFilter() 的过滤器也工作 如果..else 没有帮助就把它放在外面 另外,我注意到您正在尝试使用 Reader 读取 XML,根据我的经验,这通常是错误的。您应该在 InputStream 上使用 XML 解析器。我当然希望您不要使用正则表达式或类似的东西从 XML 中提取值?我们可以看看你的 getBaId() 和 getBody() 方法吗? 我只需要一个参数,我认为正则表达式可能比解析整个XML更快更有效。 【参考方案1】:

request.getInputStream() 只允许读取一次。为了多次使用此方法,我们需要对 HttpServletReqeustWrapper 类执行额外的自定义任务。请参阅下面的示例包装类。

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper 
    private ByteArrayOutputStream cachedBytes;

    public MultiReadHttpServletRequest(HttpServletRequest request) 
        super(request);
    

    @Override
    public ServletInputStream getInputStream() throws IOException 
        if (cachedBytes == null)
            cacheInputStream();

        return new CachedServletInputStream();
    

    @Override
    public BufferedReader getReader() throws IOException 
        return new BufferedReader(new InputStreamReader(getInputStream()));
    

    private void cacheInputStream() throws IOException 
        /*
         * Cache the inputstream in order to read it multiple times. For convenience, I use apache.commons IOUtils
         */
        cachedBytes = new ByteArrayOutputStream();
        IOUtils.copy(super.getInputStream(), cachedBytes);
    

    /* An inputstream which reads the cached request body */
    public class CachedServletInputStream extends ServletInputStream 
        private ByteArrayInputStream input;

        public CachedServletInputStream() 
            /* create a new input stream from the cached request body */
            input = new ByteArrayInputStream(cachedBytes.toByteArray());
        

        @Override
        public int read() throws IOException 
            return input.read();
        
    

就我而言,我将所有传入请求都跟踪到日志中。我创建了一个过滤器

public class TracerRequestFilter implements Filter 
    private static final Logger LOG = LoggerFactory.getLogger(TracerRequestFilter.class);

    @Override
    public void destroy() 

    

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

        try 
            if (LOG.isDebugEnabled()) 
                final MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(req);
                // debug payload info
                logPayLoad(wrappedRequest);
                chain.doFilter(wrappedRequest, response);
             else 
                chain.doFilter(request, response);
            
         finally 
            LOG.info("end-of-process");
        
    

    private String getRemoteAddress(HttpServletRequest req) 
        String ipAddress = req.getHeader("X-FORWARDED-FOR");
        if (ipAddress == null) 
            ipAddress = req.getRemoteAddr();
        
        return ipAddress;
    

    private void logPayLoad(HttpServletRequest request) 
        final StringBuilder params = new StringBuilder();
        final String method = request.getMethod().toUpperCase();
        final String ipAddress = getRemoteAddress(request);
        final String userAgent = request.getHeader("User-Agent");
        LOG.debug(String.format("============debug request=========="));
        LOG.debug(String.format("Access from ip:%s;ua:%s", ipAddress, userAgent));
        LOG.debug(String.format("Method : %s requestUri %s", method, request.getRequestURI()));
        params.append("Query Params:").append(System.lineSeparator());
        Enumeration<String> parameterNames = request.getParameterNames();

        for (; parameterNames.hasMoreElements();) 
            String paramName = parameterNames.nextElement();
            String paramValue = request.getParameter(paramName);
            if ("password".equalsIgnoreCase(paramName) || "pwd".equalsIgnoreCase(paramName)) 
                paramValue = "*****";
            
            params.append("---->").append(paramName).append(": ").append(paramValue).append(System.lineSeparator());
        
        LOG.debug(params.toString());
        /** request body */

        if ("POST".equals(method) || "PUT".equals(method)) 
            try 
                LOG.debug(IOUtils.toString(request.getInputStream()));
             catch (IOException e) 
                LOG.error(e.getMessage(), e);
            
        
        LOG.debug(String.format("============End-debug-request=========="));
    

    @Override
    public void init(FilterConfig arg0) throws ServletException 

    

Servlet 2.5 和 3.0 都适用于我。我看到所有请求参数,包括表单编码和请求 json 正文。

【讨论】:

【参考方案2】:

对于 Servlet 3.1

class MyHttpServletRequestWrapper extends HttpServletRequestWrapper 

    private byte[] body;

    public MyHttpServletRequestWrapper(HttpServletRequest request) 
        super(request);
        try 
            body = IOUtils.toByteArray(request.getInputStream());
         catch (IOException ex) 
            body = new byte[0];
        
    

    @Override
    public ServletInputStream getInputStream() throws IOException 

        return new DelegatingServletInputStream(new ByteArrayInputStream(body));


    




public class DelegatingServletInputStream extends ServletInputStream 

    private final InputStream sourceStream;

    private boolean finished = false;


    /**
     * Create a DelegatingServletInputStream for the given source stream.
     *
     * @param sourceStream the source stream (never @code null)
     */
    public DelegatingServletInputStream(InputStream sourceStream) 
        this.sourceStream = sourceStream;
    

    /**
     * Return the underlying source stream (never @code null).
     */
    public final InputStream getSourceStream() 
        return this.sourceStream;
    


    @Override
    public int read() throws IOException 
        int data = this.sourceStream.read();
        if (data == -1) 
            this.finished = true;
        
        return data;
    

    @Override
    public int available() throws IOException 
        return this.sourceStream.available();
    

    @Override
    public void close() throws IOException 
        super.close();
        this.sourceStream.close();
    

    @Override
    public boolean isFinished() 
        return this.finished;
    

    @Override
    public boolean isReady() 
        return true;
    

    @Override
    public void setReadListener(ReadListener readListener) 
        throw new UnsupportedOperationException();
    


【讨论】:

异步请求会发生什么。因为您从 setReadListener 抛出 UnsupportedOperationException。 @MaheshBhuva 你可以自己实现setReadListener 能否提供setReadListener的自定义实现参考?如果我将 setReadListener 实现留空,那么 servlet 异步功能将不起作用。我说的对吗? 我还有一个问题。如果我们通过添加一些字段来修改inputstream(如果是json body),在multipart request的情况下会不会导致任何问题? @MaheshBhuva 您显示仅当请求是 json 请求时才修改 json 正文,您可以使用 content-type 标头来验证此请求是否为 json 请求【参考方案3】:

servlet请求中的inputStream因为是流,所以只能使用一次,可以存储,然后从字节数组中获取,这样可以解决。

public class HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper 

private final byte[] body;

public HttpServletRequestWrapper(HttpServletRequest request)
        throws IOException 
    super(request);
    body = StreamUtil.readBytes(request.getReader(), "UTF-8");


@Override
public BufferedReader getReader() throws IOException 
    return new BufferedReader(new InputStreamReader(getInputStream()));


@Override
public ServletInputStream getInputStream() throws IOException 
    final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
    return new ServletInputStream() 

        @Override
        public int read() throws IOException 
            return byteArrayInputStream.read();
        

        @Override
        public boolean isFinished() 
            return false;
        

        @Override
        public boolean isReady() 
            return false;
        

        @Override
        public void setReadListener(ReadListener arg0) 
        
    ;


在过滤器中:

ServletRequest requestWrapper = new HttpServletRequestWrapper(request);

【讨论】:

但是当我们将 setReadListener 方法留空时,它是否适用于异步 servlet?。【参考方案4】:

这对我有用。它实现了getInputStream

private class MyHttpServletRequestWrapper extends HttpServletRequestWrapper 

    private byte[] body;

    public MyHttpServletRequestWrapper(HttpServletRequest request) 
        super(request);
        try 
            body = IOUtils.toByteArray(request.getInputStream());
         catch (IOException ex) 
            body = new byte[0];
        
    

    @Override
    public ServletInputStream getInputStream() throws IOException 
        return new ServletInputStream() 
            ByteArrayInputStream bais = new ByteArrayInputStream(body);

            @Override
            public int read() throws IOException 
                return bais.read();
            
        ;
    


然后你在你的方法中使用:

//copy body
servletRequest = new MyHttpServletRequestWrapper(servletRequest);

【讨论】:

Request有Multipart文件上传请求怎么处理? 这适用于任何内容。缺点是:所有内容都保存在内存中,因此内存限制可能是个问题。所以只在需要的地方创建包装器。例如如果是多部分,则传递原始请求并且不准备任何内容。【参考方案5】:

基于接受的答案的工作代码。

public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper 

private static final Logger logger = Logger.getLogger(CustomHttpServletRequestWrapper.class);
private final String body;

public CustomHttpServletRequestWrapper(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)   
        logger.error("Error reading the request body...");  
     finally   
        if (bufferedReader != null)   
            try   
                bufferedReader.close();  
             catch (IOException ex)   
                logger.error("Error closing bufferedReader...");  
              
          
      

    body = stringBuilder.toString();  


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

    ServletInputStream inputStream = new ServletInputStream()   
        public int read () throws IOException   
            return byteArrayInputStream.read();  
          
    ;

    return inputStream;  
 

【讨论】:

谢谢。使用 Spring,我也需要覆盖 getReader()public BufferedReader getReader() throws IOException return new BufferedReader(new StringReader(body)); 这是否也需要过滤器,或者我可以直接使用这个类并创建一个新对象然后转换为字符串? PS:我对 java 很陌生,需要这样做才能从请求中打印我的 JSON。【参考方案6】:

您可能开始使用 getReader() 来使用 HttpServletRequest :

String ba = getBaId(getBody(httpRequest)); 

您的 servlet 尝试在同一请求上调用 getInputStream(),这是不允许的。您需要做的是使用ServletRequestWrapper 制作请求正文的副本,以便您可以使用多种方法阅读它。我没有时间找到一个完整的例子知道...对不起...

【讨论】:

拥有请求的副本并使用getReader() 没有帮助。例外是getReader() has already been called for this request

以上是关于如何多次读取 request.getInputStream()的主要内容,如果未能解决你的问题,请参考以下文章

如何读取 BufferedReader 两次或多次?

如何多次写入/读取管道

如何在 C# .NET 中保持 TCP 连接打开并执行多次写入/读取?

如何优化打开和读取多次相同文件的python脚本?

如何在 asp net core 2.2 中间件中多次读取请求正文?

如何只读取一次 JSON 并在 Robot Framework 的同一个机器人文件中多次使用它