предисловие
Иногда нам нужно использовать перехватчики для перехвата данных в потоке запроса или ответа и чтения некоторой информации в нем, может быть, для извлечения журнала, может быть, для выполнения некоторой проверки, но когда мы читаем поток запроса или обратного вызова после данные будут собраны, обнаружится, что эти потоковые данные не могут быть снова использованы ниже по течению.На самом деле есть две потенциальные ямы.
яма один
Методы запроса getInputStream(), getReader() и getParameter() являются взаимоисключающими, то есть, если используется один из них и используются два других, данные не могут быть получены. Помимо взаимного исключения, и getInputStream(), и getReader() можно использовать только один раз, а getParameter можно использовать повторно в одном потоке.
Причины взаимного исключения трех методов
Метод org.apache.catalina.connector.Request реализует интерфейс javax.servlet.http.HttpServletRequest Давайте взглянем на реализацию этих трех методов:
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;
}
Сначала посмотрите на два метода, getInputStream() и getReader(). Вы можете видеть, что флаги usingReader и usingInputStream используются для ограничения чтения потока. Взаимное исключение этих двух методов хорошо понятно. Давайте посмотрим, как метод getParameter() является взаимоисключающим с ними.
getParameter
@Override
public String getParameter(String name) {
// 只会解析一遍Parameter
if (!parametersParsed) {
parseParameters();
}
// 从coyoteRequest中获取参数
return coyoteRequest.getParameters().getParameter(name);
}
На первый взгляд кажется, что взаимного исключения нет. Не волнуйтесь, продолжайте смотреть вниз. Давайте перейдем к методу parseParameters(), чтобы посмотреть (можно прямо посмотреть на среднюю часть исходного кода):
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);
}
}
}
Таким образом, это показывает, что метод getParameter() не может быть прочитан по желанию. Так почему же они оба читаются только один раз?
Причина, по которой его можно прочитать только один раз
Оба метода getInputStream() и getReader() могут быть прочитаны только один раз, а getParameter() можно повторно использовать в одном потоке, главным образом потому, что getParameter() будет анализировать данные в потоке и сохранять их в LinkedHashMap. Параметр можно увидеть в инкапсуляции в классе Parameters.В исходном коде метода parseParameters() выше вы также можете увидеть, что объект Parameters генерируется в начале. Последующие данные чтения сохраняются в этом объекте. Однако методы getInputStream() и getReader() этого не делают. Метод getInputStream() возвращает CoyoteInputStream, а getReader() возвращает CoyoteReader. CoyoteInputStream наследует InputStream, а CoyoteReader наследует BufferedReader. Из исходного кода InputStream и BufferedReader записывают данные после чтение данных Координаты прочитанных данных не будут сброшены, потому что ни CoyoteInputStream, ни CoyoteReader не реализуют метод сброса, в результате чего данные считываются только один раз.
яма два
Ответ такой же, как и запрос, методы getOutputStream() и getWriter() являются взаимоисключающими, а данные тела в ответе могут использоваться только один раз.
взаимоисключающие причины
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;
}
Причина, по которой его можно прочитать только один раз
В ответе чтение означает повторное чтение данных тела из OutputStream, а OutputStream также имеет ту же проблему, что и InputStream.Поток можно прочитать только один раз, поэтому я не буду вдаваться в подробности.
решение
В библиотеке Spring предусмотрено два класса, ContentCachingResponseWrapper и ContentCachingRequestWrapper, которые соответственно решают проблемы, связанные с тем, что Response и Request нельзя читать повторно, а методы являются взаимоисключающими. Мы можем напрямую использовать ContentCachingRequestWrapper для переноса запроса и ContentCachingResponseWrapper для переноса ответа. После переноса мы будем кэшировать копию данных при чтении данных потока. После чтения мы можем переписать данные потока в запрос или ответ. Вот простой пример использования:
ContentCachingResponseWrapper responseToCache = new ContentCachingResponseWrapper(response);
String responseBody = new String(responseToCache.getContentAsByteArray());
responseToCache.copyBodyToResponse();
Кэшировать поток данных, это базовое решение, давайте посмотрим на уровень исходного кода, в основном сосредоточимся на методах getContentAsByteArray(), copyBodyToResponse():
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();
}
}
}
Решение ContentCachingRequestWrapper похоже, я не буду его здесь раскрывать, а кому интересно, могут посмотреть исходный код напрямую.