You must not know about HttpServletResponse and HttpServletRequest value of two pit

Foreword

Sometimes, we need to use interceptors to Request or Response data flow inside intercept, read some of the inside information, perhaps as a log retrieval, maybe do some checking, but when we read in the request or callback stream after the data, you will find these data in the downstream flow can not be consumed again, there is the fact there are two potential pit.

A pit

Request the getInputStream (), getReader (), getParameter () method mutually exclusive, i.e. one of which is used, then use the other two, the data is not acquired. In addition to exclusive outside, the getInputStream () and getReader () can only be used once, it can be reused on a single thread getParameter.

Three methods are mutually exclusive reasons

org.apache.catalina.connector.Request method achieves javax.servlet.http.HttpServletRequest interfaces, we look at the implementation of these three methods:

getInputStream

@Override
public ServletInputStream getInputStream() throws IOException {

    if (usingReader) {
        throw new IllegalStateException
            (sm.getString("coyoteRequest.getInputStream.ise"));
    }

    usingInputStream = true;
    if (inputStream == null) {
        inputStream = new CoyoteInputStream(inputBuffer);
    }
    return inputStream;

}
复制代码

getReader

@Override
public BufferedReader getReader() throws IOException {

    if (usingInputStream) {
        throw new IllegalStateException
            (sm.getString("coyoteRequest.getReader.ise"));
    }

    usingReader = true;
    inputBuffer.checkConverter();
    if (reader == null) {
        reader = new CoyoteReader(inputBuffer);
    }
    return reader;

}
复制代码

First look getInputStream () and getReader () These two methods can be seen, respectively usingReader and usingInputStream signs have been restricted in the reading flow, exclusive of these two methods is well understood. Take a look below getParameter () method is how mutually exclusive with them.

getParameter

@Override
public String getParameter(String name) {
		// 只会解析一遍Parameter
    if (!parametersParsed) {
        parseParameters();
    }
  	// 从coyoteRequest中获取参数
    return coyoteRequest.getParameters().getParameter(name);

}
复制代码

At first glance does not seem mutually exclusive, do not worry, continue to look down, we entered parseParameters () method to take a look (you can look directly at the middle portion of the source code):

protected void parseParameters() {
		//标识位,标志已经被解析过。
    parametersParsed = true;
		
    Parameters parameters = coyoteRequest.getParameters();
    boolean success = false;
    try {
        // Set this every time in case limit has been changed via JMX
        parameters.setLimit(getConnector().getMaxParameterCount());

        // getCharacterEncoding() may have been overridden to search for
        // hidden form field containing request encoding
        String enc = getCharacterEncoding();

        boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
        if (enc != null) {
            parameters.setEncoding(enc);
            if (useBodyEncodingForURI) {
                parameters.setQueryStringEncoding(enc);
            }
        } else {
            parameters.setEncoding
                (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
            if (useBodyEncodingForURI) {
                parameters.setQueryStringEncoding
                    (org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
            }
        }

        parameters.handleQueryParameters();
				// 重点看这里:这里会判断是否有读取过流。如果有,则直接return。
        if (usingInputStream || usingReader) {
            success = true;
            return;
        }

        if( !getConnector().isParseBodyMethod(getMethod()) ) {
            success = true;
            return;
        }

        String contentType = getContentType();
        if (contentType == null) {
            contentType = "";
        }
        int semicolon = contentType.indexOf(';');
        if (semicolon >= 0) {
            contentType = contentType.substring(0, semicolon).trim();
        } else {
            contentType = contentType.trim();
        }

        if ("multipart/form-data".equals(contentType)) {
            parseParts(false);
            success = true;
            return;
        }

        if (!("application/x-www-form-urlencoded".equals(contentType))) {
            success = true;
            return;
        }

        int len = getContentLength();

        if (len > 0) {
            int maxPostSize = connector.getMaxPostSize();
            if ((maxPostSize > 0) && (len > maxPostSize)) {
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.postTooLarge"));
                }
                checkSwallowInput();
                return;
            }
            byte[] formData = null;
            if (len < CACHED_POST_LEN) {
                if (postData == null) {
                    postData = new byte[CACHED_POST_LEN];
                }
                formData = postData;
            } else {
                formData = new byte[len];
            }
            try {
                if (readPostBody(formData, len) != len) {
                    return;
                }
            } catch (IOException e) {
                // Client disconnect
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"),
                            e);
                }
                return;
            }
            parameters.processParameters(formData, 0, len);
        } else if ("chunked".equalsIgnoreCase(
                coyoteRequest.getHeader("transfer-encoding"))) {
            byte[] formData = null;
            try {
                formData = readChunkedPostBody();
            } catch (IOException e) {
                // Client disconnect or chunkedPostTooLarge error
                Context context = getContext();
                if (context != null && context.getLogger().isDebugEnabled()) {
                    context.getLogger().debug(
                            sm.getString("coyoteRequest.parseParameters"),
                            e);
                }
                return;
            }
            if (formData != null) {
                parameters.processParameters(formData, 0, formData.length);
            }
        }
        success = true;
    } finally {
        if (!success) {
            parameters.setParseFailed(true);
        }
    }

}
复制代码

As a result, it shows the getParameter () method can not be arbitrarily read. So why they can only be read once it?

The reason can only be read once

the getInputStream () and getReader () method can only be read once, and getParameter () is reusable on a single thread, mainly because getParameter () parses the data stream will be stored in a LinkedHashMap, the relevant Parameters can see the contents of the package class, in the above parseParameters () method of the source can also be seen to generate a start Parameters object. Subsequent read data are present in the object. But getInputStream () and getReader () method will not do so, getInputStream () method returns CoyoteInputStream, getReader () returns CoyoteReader, CoyoteInputStream inherited InputStream, CoyoteReader inherited BufferedReader, InputStream and BufferedReader see read data from the source, the record read coordinate data will not be reset because CoyoteInputStream and CoyoteReader have not achieved the reset method, which results in data can only be read once.

Two pit

Response to the Request, like, getOutputStream () and getWriter () method is mutually exclusive, and the body of the Response data can only consume time.

Mutually exclusive reasons

getOutputStream

@Override
public ServletOutputStream getOutputStream()
    throws IOException {

    if (usingWriter) {
        throw new IllegalStateException
            (sm.getString("coyoteResponse.getOutputStream.ise"));
    }

    usingOutputStream = true;
    if (outputStream == null) {
        outputStream = new CoyoteOutputStream(outputBuffer);
    }
    return outputStream;

}
复制代码

getWriter

@Override
public PrintWriter getWriter()
    throws IOException {

    if (usingOutputStream) {
        throw new IllegalStateException
            (sm.getString("coyoteResponse.getWriter.ise"));
    }

    if (ENFORCE_ENCODING_IN_GET_WRITER) {
        setCharacterEncoding(getCharacterEncoding());
    }

    usingWriter = true;
    outputBuffer.checkConverter();
    if (writer == null) {
        writer = new CoyoteWriter(outputBuffer);
    }
    return writer;
}
复制代码

The reason can only be read once

In the Response, the reading means re-read the data from the OutputStream body, but also OutputStream and InputStream same problems, traffic can only be read once, do not start here to talk about it.

solution

In the Spring library provides two classes ContentCachingResponseWrapper and ContentCachingRequestWrapper, respectively, to solve the Response and Request can not read and repeat method mutually exclusive issues. We can use directly ContentCachingRequestWrapper packaging Request, ContentCachingResponseWrapper Response to wrap, after packing, this will be a data cache, so after reading it, and then re-write the data stream at the time of Request or Response stream to read data on it . The following is a simple example of use:

ContentCachingResponseWrapper responseToCache = new ContentCachingResponseWrapper(response);
String responseBody = new String(responseToCache.getContentAsByteArray());
responseToCache.copyBodyToResponse();
复制代码

A stream of data cache, which is the basic solution ideas, let's take a look at the source code level, focusing getContentAsByteArray (), copyBodyToResponse () method on the line:

public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {

   private final FastByteArrayOutputStream content = new FastByteArrayOutputStream(1024);

   private final ServletOutputStream outputStream = new ResponseServletOutputStream();

   private PrintWriter writer;

   private int statusCode = HttpServletResponse.SC_OK;

   private Integer contentLength;


   /**
    * Create a new ContentCachingResponseWrapper for the given servlet response.
    * @param response the original servlet response
    */
   public ContentCachingResponseWrapper(HttpServletResponse response) {
      super(response);
   }


   @Override
   public void setStatus(int sc) {
      super.setStatus(sc);
      this.statusCode = sc;
   }

   @SuppressWarnings("deprecation")
   @Override
   public void setStatus(int sc, String sm) {
      super.setStatus(sc, sm);
      this.statusCode = sc;
   }

   @Override
   public void sendError(int sc) throws IOException {
      copyBodyToResponse(false);
      try {
         super.sendError(sc);
      }
      catch (IllegalStateException ex) {
         // Possibly on Tomcat when called too late: fall back to silent setStatus
         super.setStatus(sc);
      }
      this.statusCode = sc;
   }

   @Override
   @SuppressWarnings("deprecation")
   public void sendError(int sc, String msg) throws IOException {
      copyBodyToResponse(false);
      try {
         super.sendError(sc, msg);
      }
      catch (IllegalStateException ex) {
         // Possibly on Tomcat when called too late: fall back to silent setStatus
         super.setStatus(sc, msg);
      }
      this.statusCode = sc;
   }

   @Override
   public void sendRedirect(String location) throws IOException {
      copyBodyToResponse(false);
      super.sendRedirect(location);
   }

   @Override
   public ServletOutputStream getOutputStream() throws IOException {
      return this.outputStream;
   }

   @Override
   public PrintWriter getWriter() throws IOException {
      if (this.writer == null) {
         String characterEncoding = getCharacterEncoding();
         this.writer = (characterEncoding != null ? new ResponsePrintWriter(characterEncoding) :
               new ResponsePrintWriter(WebUtils.DEFAULT_CHARACTER_ENCODING));
      }
      return this.writer;
   }

   @Override
   public void flushBuffer() throws IOException {
      // do not flush the underlying response as the content as not been copied to it yet
   }

   @Override
   public void setContentLength(int len) {
      if (len > this.content.size()) {
         this.content.resize(len);
      }
      this.contentLength = len;
   }

   // Overrides Servlet 3.1 setContentLengthLong(long) at runtime
   public void setContentLengthLong(long len) {
      if (len > Integer.MAX_VALUE) {
         throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" +
               Integer.MAX_VALUE + "): " + len);
      }
      int lenInt = (int) len;
      if (lenInt > this.content.size()) {
         this.content.resize(lenInt);
      }
      this.contentLength = lenInt;
   }

   @Override
   public void setBufferSize(int size) {
      if (size > this.content.size()) {
         this.content.resize(size);
      }
   }

   @Override
   public void resetBuffer() {
      this.content.reset();
   }

   @Override
   public void reset() {
      super.reset();
      this.content.reset();
   }

   /**
    * Return the status code as specified on the response.
    */
   public int getStatusCode() {
      return this.statusCode;
   }

   /**
    * Return the cached response content as a byte array.
    */
   public byte[] getContentAsByteArray() {
      return this.content.toByteArray();
   }

   /**
    * Return an {@link InputStream} to the cached content.
    * @since 4.2
    */
   public InputStream getContentInputStream() {
      return this.content.getInputStream();
   }

   /**
    * Return the current size of the cached content.
    * @since 4.2
    */
   public int getContentSize() {
      return this.content.size();
   }

   /**
    * Copy the complete cached body content to the response.
    * @since 4.2
    */
   public void copyBodyToResponse() throws IOException {
      copyBodyToResponse(true);
   }

   /**
    * Copy the cached body content to the response.
    * @param complete whether to set a corresponding content length
    * for the complete cached body content
    * @since 4.2
    */
   protected void copyBodyToResponse(boolean complete) throws IOException {
      if (this.content.size() > 0) {
         HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
         if ((complete || this.contentLength != null) && !rawResponse.isCommitted()) {
            rawResponse.setContentLength(complete ? this.content.size() : this.contentLength);
            this.contentLength = null;
         }
         this.content.writeTo(rawResponse.getOutputStream());
         this.content.reset();
         if (complete) {
            super.flushBuffer();
         }
      }
   }


   private class ResponseServletOutputStream extends ServletOutputStream {

      @Override
      public void write(int b) throws IOException {
         content.write(b);
      }

      @Override
      public void write(byte[] b, int off, int len) throws IOException {
         content.write(b, off, len);
      }
   }


   private class ResponsePrintWriter extends PrintWriter {

      public ResponsePrintWriter(String characterEncoding) throws UnsupportedEncodingException {
         super(new OutputStreamWriter(content, characterEncoding));
      }

      @Override
      public void write(char buf[], int off, int len) {
         super.write(buf, off, len);
         super.flush();
      }

      @Override
      public void write(String s, int off, int len) {
         super.write(s, off, len);
         super.flush();
      }

      @Override
      public void write(int c) {
         super.write(c);
         super.flush();
      }
   }

}
复制代码

The ContentCachingRequestWrapper thinking of settlement is almost, I will not start here, are interested can directly view the source code.

Send welfare area

Scanning the next Fanger Wei code number [of public concern tune the code to add a little seasoning]
Click on the menu bar for a free 49 of "Dubbo source analysis" series

Guess you like

Origin juejin.im/post/5e6aee11e51d452703137ce6