HttpServletResponse和HttpServletRequest的坑

有时候,我们需要用拦截器对Request或者Response流里面的数据进行拦截,读取里面的一些信息,也许是作为日志检索,也许是做一些校验,但是当我们读取里请求或者回调的流数据后,会发现这些流数据在下游就无法再次被消费了,这里面是其实存在着两个潜在的坑。

坑一

Request的 getInputStream()、getReader()、getParameter()方法互斥,也就是使用了其中一个,再使用另外的两,是获取不到数据的。
除了互斥外,getInputStream()和getReader()都只能使用一次,getParameter单线程上可重复使用。

三个方法互斥原因

org.apache.catalina.connector.Request方法实现了javax.servlet.http.HttpServletRequest接口,我们来看看这三个方法的实现:
getInputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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

1
2
3
4
5
6
7
8
9
10
@Override
public String getParameter(String name) {
// 只会解析一遍Parameter
if (!parametersParsed) {
parseParameters();
}
// 从coyoteRequest中获取参数
return coyoteRequest.getParameters().getParameter(name);

}

粗略一看好像没有互斥,别着急,继续往下看,我们进到parseParameters()方法中来看一看(可以直接看源码中间部分):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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都没有实现reset方法,这导致数据只能被读取一次。

ServletRequest#getInputStream()只能读取一次,一般在filter中从request的inputStream读取数据后,会在controller层抛出异常,例如“Required request body is missing”。

坑二

Response与Request一样,getOutputStream()和getWriter()方法也是互斥的,并且Response中的body数据也只能消费一次。

互斥原因

getOutputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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就可以了。下面是一个简单的使用示例:

1
2
3
ContentCachingResponseWrapper responseToCache = new ContentCachingResponseWrapper(response);
String responseBody = new String(responseToCache.getContentAsByteArray());
responseToCache.copyBodyToResponse();

缓存一份流数据,这就是基本的解决思路,下面我们从源码层面来看一看,主要关注getContentAsByteArray()、copyBodyToResponse()方法就行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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的解决思路也是差不多,这里就不展开了,有兴趣的可以直接查看源码。


转载自:
https://juejin.im/post/5e6aee11e51d452703137ce6