解決HttpServletResponse和HttpServletRequest取值的2個(gè)坑
有時(shí)候,我們需要用攔截器對Request或者Response流里面的數(shù)據(jù)進(jìn)行攔截,讀取里面的一些信息,也許是作為日志檢索,也許是做一些校驗(yàn),但是當(dāng)我們讀取里請求或者回調(diào)的流數(shù)據(jù)后,會發(fā)現(xiàn)這些流數(shù)據(jù)在下游就無法再次被消費(fèi)了,這里面是其實(shí)存在著兩個(gè)潛在的坑。
坑一
Request的 getInputStream()、getReader()、getParameter()方法互斥,也就是使用了其中一個(gè),再使用另外的兩,是獲取不到數(shù)據(jù)的。
除了互斥外,getInputStream()和getReader()都只能使用一次,getParameter單線程上可重復(fù)使用。
三個(gè)方法互斥原因
org.apache.catalina.connector.Request方法實(shí)現(xiàn)了javax.servlet.http.HttpServletRequest接口,我們來看看這三個(gè)方法的實(shí)現(xiàn):
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()這兩個(gè)方法,可以看到,在讀流時(shí)分別用usingReader和usingInputStream標(biāo)志做了限制,這兩個(gè)方法的互斥很好理解。
下面看一看getParameter()方法是怎么跟他們互斥的。
getParameter
@Override
public String getParameter(String name) {
// 只會解析一遍Parameter
if (!parametersParsed) {
parseParameters();
}
// 從coyoteRequest中獲取參數(shù)
return coyoteRequest.getParameters().getParameter(name);
}粗略一看好像沒有互斥,別著急,繼續(xù)往下看,我們進(jìn)到parseParameters()方法中來看一看(可以直接看源碼中間部分):
protected void parseParameters() {
//標(biāo)識位,標(biāo)志已經(jīng)被解析過。
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();
// 重點(diǎn)看這里:這里會判斷是否有讀取過流。如果有,則直接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()是在單線程上可重復(fù)使用,主要是因?yàn)間etParameter()中會解析流中的數(shù)據(jù)后存放在了一個(gè)LinkedHashMap中,相關(guān)的內(nèi)容可以看Parameters類中的封裝,在上面parseParameters()方法的源碼中也可以看到一開始就生成了一個(gè)Parameters對象。
后續(xù)讀取的數(shù)據(jù)都存在了這個(gè)對象中。但是getInputStream()和getReader()方法就沒有這樣做,getInputStream()方法返回CoyoteInputStream,getReader()返回CoyoteReader,CoyoteInputStream繼承了InputStream,CoyoteReader繼承了BufferedReader,從源碼看InputStream和BufferedReader在讀取數(shù)據(jù)后,記錄數(shù)據(jù)讀取的坐標(biāo)不會被重置,因?yàn)镃oyoteInputStream和CoyoteReader都沒有實(shí)現(xiàn)reset方法,這導(dǎo)致數(shù)據(jù)只能被讀取一次。
坑二
Response與Request一樣,getOutputStream()和getWriter()方法也是互斥的,并且Response中的body數(shù)據(jù)也只能消費(fèi)一次。
互斥原因
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數(shù)據(jù)讀出來,而OutputStream也和InputStream存在同樣的問題,流只能讀取一次,這里就不展開講了。
解決方案
在Spring庫中,提供了ContentCachingResponseWrapper和ContentCachingRequestWrapper兩個(gè)類,分別解決了Response和Request不能重復(fù)讀以及方法互斥問題。
我們可以直接用ContentCachingRequestWrapper來包裝Request,ContentCachingResponseWrapper來包裝Response,包裝后,在讀取流數(shù)據(jù)的時(shí)候會將這個(gè)數(shù)據(jù)緩存一份,等讀完以后,再將流數(shù)據(jù)重新寫入Request或者Response就可以了。
下面是一個(gè)簡單的使用示例:
ContentCachingResponseWrapper responseToCache = new ContentCachingResponseWrapper(response); String responseBody = new String(responseToCache.getContentAsByteArray()); responseToCache.copyBodyToResponse();
緩存一份流數(shù)據(jù),這就是基本的解決思路,下面我們從源碼層面來看一看,主要關(guān)注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的解決思路也是差不多。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java排查一個(gè)線上死循環(huán)cpu暴漲的過程分析
這篇文章主要介紹了java排查一個(gè)線上死循環(huán)cpu暴漲的過程分析,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08
Java8(291)之后禁用了TLS1.1使JDBC無法用SSL連接SqlServer2008的解決方法
這篇文章主要介紹了Java8(291)之后禁用了TLS1.1使JDBC無法用SSL連接SqlServer2008的解決方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
SpringBoot啟動報(bào)錯屬性循環(huán)依賴報(bào)錯問題的解決
這篇文章主要介紹了SpringBoot啟動報(bào)錯屬性循環(huán)依賴報(bào)錯問題的解決方案,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05
Spring Cloud入門系列服務(wù)提供者總結(jié)
這篇文章主要介紹了Spring Cloud入門系列之服務(wù)提供者總結(jié),服務(wù)提供者使用Eureka Client組件創(chuàng)建 ,創(chuàng)建完成以后修改某文件,具體操作方法及實(shí)例代碼跟隨小編一起看看吧2021-06-06
java線程中synchronized和Lock區(qū)別及介紹
這篇文章主要為大家介紹了java線程中synchronized和Lock區(qū)別及介紹,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06
java微信企業(yè)號開發(fā)之開發(fā)模式的開啟
這篇文章主要為大家詳細(xì)介紹了java微信企業(yè)號開發(fā)之開發(fā)模式的開啟方法,感興趣的小伙伴們可以參考一下2016-06-06

