preface
Sometimes, we need to use interceptors to intercept the data in the Request or Response stream and read some information, maybe for log retrieval, maybe for verification, but when we read the data in the Request or callback stream, we will find that the stream data cannot be consumed in the downstream. There are two potential pitfalls.
A pit
The Request getInputStream(), getReader(), and getParameter() methods are mutually exclusive. If one of them is used and the other two are used, no data can be obtained. In addition to the mutexes, both getInputStream() and getReader() can only be used once, and getParameter can be used repeatedly on a single thread.
The three methods are mutually exclusive
Org. Apache. Catalina. Connector. Request method to realize the javax.mail. Servlet. HTTP. It interfaces, we come to the realization of the three methods:
getInputStream
@Override
public ServletInputStream getInputStream(a) throws IOException {
if (usingReader) {
throw new IllegalStateException
(sm.getString("coyoteRequest.getInputStream.ise"));
}
usingInputStream = true;
if (inputStream == null) {
inputStream = new CoyoteInputStream(inputBuffer);
}
return inputStream;
}
Copy the code
getReader
@Override
public BufferedReader getReader(a) 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;
}
Copy the code
GetInputStream () and getReader() are the two methods that restrict reading of streams with the usingReader and usingInputStream flags, respectively. The two methods are mutually exclusive and understandable. Let’s take a look at how the getParameter() method is mutually exclusive.
getParameter
@Override
public String getParameter(String name) {
// Parameter is parsed only once
if(! parametersParsed) { parseParameters(); }// Get parameters from coyoteRequest
return coyoteRequest.getParameters().getParameter(name);
}
Copy the code
At first glance, there seems to be no mutex, so don’t worry, let’s dive into the parseParameters() method to take a look (you can look directly at the middle of the source code) :
protected void parseParameters(a) {
// Indicates that the bit has been parsed.
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();
// Check whether the stream has been read. If yes, return directly.
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); }}}Copy the code
This means that the getParameter() method cannot be read arbitrarily. So why can they all only be read once?
The reason for reading only once
The getInputStream() and getReader() methods can only be read once, while getParameter() is reusable on a single thread, mainly because getParameter() parses the data in the stream and stores it in a LinkedHashMap. See the encapsulation in the Parameters class, and you can see in the source code for the parseParameters() method above that a Parameters object is generated at the start. Subsequent readings are stored in this object. But the getInputStream() and getReader() methods don’t do this. GetInputStream () returns CoyoteInputStream, getReader() returns CoyoteReader, CoyoteInputStream inherits from InputStream, and CoyoteReader inherits from BufferedReader. Because neither CoyoteInputStream nor CoyoteReader implements the reset method, data can only be read once.
Pit 2
As with Request, the getOutputStream() and getWriter() methods are mutually exclusive, and the body data in Response can only be consumed once.
The mutex reason
getOutputStream
@Override
public ServletOutputStream getOutputStream(a)
throws IOException {
if (usingWriter) {
throw new IllegalStateException
(sm.getString("coyoteResponse.getOutputStream.ise"));
}
usingOutputStream = true;
if (outputStream == null) {
outputStream = new CoyoteOutputStream(outputBuffer);
}
return outputStream;
}
Copy the code
getWriter
@Override
public PrintWriter getWriter(a)
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;
}
Copy the code
The reason for reading only once
In Response, reading means re-reading the body data from OutputStream. OutputStream also has the same problem as InputStream. The stream can only be read once, so I won’t expand on it here.
The solution
In the Spring libraries, provides ContentCachingResponseWrapper and ContentCachingRequestWrapper two classes, respectively to solve the Response and Request cannot repeat reading method and incompatible problem. We can directly use ContentCachingRequestWrapper Request to packaging, ContentCachingResponseWrapper Response to packaging, packaging, when read flow data will be the a data cache, such as reading, Write the stream data back to Request or Response. Here is a simple usage example:
ContentCachingResponseWrapper responseToCache = new ContentCachingResponseWrapper(response);
String responseBody = new String(responseToCache.getContentAsByteArray());
responseToCache.copyBodyToResponse();
Copy the code
GetContentAsByteArray (); copyBodyToResponse(); getContentAsByteArray()
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(a) throws IOException {
return this.outputStream;
}
@Override
public PrintWriter getWriter(a) 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(a) 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;
}
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(a) {
this.content.reset();
}
@Override
public void reset(a) {
super.reset();
this.content.reset();
}
/** * Return the status code as specified on the response. */
public int getStatusCode(a) {
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.
* @since4.2 * /
public InputStream getContentInputStream(a) {
return this.content.getInputStream();
}
/**
* Return the current size of the cached content.
* @since4.2 * /
public int getContentSize(a) {
return this.content.size();
}
/**
* Copy the complete cached body content to the response.
* @since4.2 * /
public void copyBodyToResponse(a) 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
* @since4.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(); }}}Copy the code
And ContentCachingRequestWrapper solution is about the same, I am not opened here, interested can directly see the source code.
Welfare delivery area
Scan the qr code below to follow the public account
Click on the menu bar for the free 49-part Dubbo source Code series