有時候,咱們須要用攔截器對Request或者Response流裏面的數據進行攔截,讀取裏面的一些信息,也許是做爲日誌檢索,也許是作一些校驗,可是當咱們讀取裏請求或者回調的流數據後,會發現這些流數據在下游就沒法再次被消費了,這裏面是其實存在着兩個潛在的坑。java
Request的 getInputStream()、getReader()、getParameter()方法互斥,也就是使用了其中一個,再使用另外的兩,是獲取不到數據的。除了互斥外,getInputStream()和getReader()都只能使用一次,getParameter單線程上可重複使用。apache
org.apache.catalina.connector.Request方法實現了javax.servlet.http.HttpServletRequest接口,咱們來看看這三個方法的實現:緩存
getInputStreammarkdown
@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; } 複製代碼
getReaderapp
@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()方法是怎麼跟他們互斥的。ide
getParameterpost
@Override public String getParameter(String name) { // 只會解析一遍Parameter if (!parametersParsed) { parseParameters(); } // 從coyoteRequest中獲取參數 return coyoteRequest.getParameters().getParameter(name); } 複製代碼
粗略一看好像沒有互斥,彆着急,繼續往下看,咱們進到parseParameters()方法中來看一看(能夠直接看源碼中間部分):this
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()方法也不能隨意讀取的。那麼爲何它們都只能讀取一次呢?url
getInputStream()和getReader()方法都只能讀取一次,而getParameter()是在單線程上可重複使用,主要是由於getParameter()中會解析流中的數據後存放在了一個LinkedHashMap中,相關的內容能夠看Parameters類中的封裝,在上面parseParameters()方法的源碼中也能夠看到一開始就生成了一個Parameters對象。後續讀取的數據都存在了這個對象中。可是getInputStream()和getReader()方法就沒有這樣作,getInputStream()方法返回CoyoteInputStream,getReader()返回CoyoteReader,CoyoteInputStream繼承了InputStream,CoyoteReader繼承了BufferedReader,從源碼看InputStream和BufferedReader在讀取數據後,記錄數據讀取的座標不會被重置,由於CoyoteInputStream和CoyoteReader都沒有實現reset方法,這致使數據只能被讀取一次。spa
Response與Request同樣,getOutputStream()和getWriter()方法也是互斥的,而且Response中的body數據也只能消費一次。
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; } 複製代碼
在Response中,讀取是指從OutputStream中從新把body數據讀出來,而OutputStream也和InputStream存在一樣的問題,流只能讀取一次,這裏就不展開講了。
在Spring庫中,提供了ContentCachingResponseWrapper和ContentCachingRequestWrapper兩個類,分別解決了Response和Request不能重複讀以及方法互斥問題。咱們能夠直接用ContentCachingRequestWrapper來包裝Request,ContentCachingResponseWrapper來包裝Response,包裝後,在讀取流數據的時候會將這個數據緩存一份,等讀完之後,再將流數據從新寫入Request或者Response就能夠了。下面是一個簡單的使用示例:
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的解決思路也是差很少,我這裏就不展開了,有興趣的能夠直接查看源碼。