This is the 27th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
If the background
The need to log the user’s request parameters in a project for later problem finding can be done through interceptors in Spring or filters in servlets. I chose to use a filter here, which is to add a filter, get the Request object in the filter, and log the information in Reques.
Problem is introduced
Reset HttpRequest after calling Request. getReader:
Sometimes our request is POST, but we want to sign the parameters, so we need to get the body information, but when we get the parameters using getReader() and getInputStream() of HttpServletRequest, There is no way to get the body from the frame or from yourself. Of course, there are other scenarios that may require multiple fetches.
An exception like the following may be thrown
java.lang.IllegalStateException: getReader() has already been called for this request
Copy the code
Therefore, to solve this problem, the following solutions are given:
Define filter resolution
Using the filter quickly I achieved a unified record request parameters function, the whole code is implemented as follows:
@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("Request parameters :{}", JSON.toJSONString(parameterMap)); filterChain.doFilter(request,response); }}Copy the code
The above implementation does not have a problem with GET requests and can record the parameters submitted by the front end. For POST requests, it’s not so simple. The content-type Type of the POST request is:
- Application/X-www-form-urlencoded: This is the most common way, and browser native form forms are submitted in this way.
- Application/JSON: This is a common approach when submitting a complex object.
- Multipart /form-data: This is usually used when uploading files using forms.
Note: One of the three common POST methods I implemented is not logged. When the Content-Type is Application /json, the Request parameters cannot be obtained by calling the getParameter method in the Request object.
Application/JSON solutions and problems
To print this form of Request parameters, we can fetch the Request JSON Request parameters by reading the Request stream. Now modify the following code:
@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("Request parameters :{}",JSON.toJSONString(parameterMap));
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(),out);
log.info("Request body :{}", out.toString(request.getCharacterEncoding())); filterChain.doFilter(request,response); }}Copy the code
In the above code, I get the JSON data from the Request submitted to the server by obtaining the stream in the Request. Finally, I can print the JSON data submitted by the client in the log. However, the interface did not return successfully in the end, and the request parameters could not be obtained from the Controller. Finally, the program presented the key error message: Required Request body is missing.
The exception occurs because the stream in the Request can only be read once. If the stream is read again after being read in the filter, the service will be abnormal. Simply speaking, the stream obtained in the Request does not support repeated reading.
So this scheme passes
Expand it
HttpServletRequestWrapper
From the above analysis, we know the problem. For the problem that the Request stream cannot repeat reads, we need to find a way to make it support repeat reads.
If we want to implement a Request ourselves, and the stream in our Request supports repeated reads, this is a very difficult thing to do.
Fortunately the Servlet provides a HttpServletRequestWrapper class, the class can see it from the name is a Wrapper class, is we can use it to get original flow method of packing, let it supports reads.
Create a custom class
Implement a CustomHttpServletRequest inheritance HttpServletRequestWrapper and write a constructor to cache the body data, first to save RequestBody as a byte array, Then through the Servlet built-in HttpServletRequestWrapper class covers getReader () and getInputStream () method, which makes the flow from the saved read a byte array.
public class CustomHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CustomHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream is = request.getInputStream();
this.cachedBody = StreamUtils.copyToByteArray(is); }}Copy the code
Rewrite getReader ()
@Override
public BufferedReader getReader(a) throws IOException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}
Copy the code
Rewrite the getInputStream ()
@Override
public ServletInputStream getInputStream(a) throws IOException {
return new CachedBodyServletInputStream(this.cachedBody);
}
Copy the code
Then replace the ServletRequest with a ServletRequestWrapper in the Filter. The code is as follows:
Implement ServletInputStream
Create a class that inherits ServletInputStream
public class CachedBodyServletInputStream extends ServletInputStream {
private InputStream cachedBodyInputStream;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
}
@Override
public boolean isFinished(a) {
try {
return cachedBodyInputStream.available() == 0;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return false;
}
@Override
public boolean isReady(a) {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
@Override
public int read(a) throws IOException {
returncachedBodyInputStream.read(); }}Copy the code
Create a Filter and add it to the container
Now that we want to add it to the container, we can create a Filter and then add the configuration and we can simply inherit OncePerRequestFilter and implement the following method.
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
CustomHttpServletRequest customHttpServletRequest =
new CustomHttpServletRequest(httpServletRequest);
filterChain.doFilter(customHttpServletRequest, httpServletResponse);
}
Copy the code
Then add the Filter to join can, in the above filters obtained first call getParameterMap parameters, and then obtain flow, if I go getInputStream then call getParameterMap will lead to the failure argument parsing.
For example, reorder the code in the filter as follows:
@Slf4j
@Component
public class CheckDataFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// Replace the original Request with a wrapper Request
request = new CustomHttpServletRequest(request);
// Read the contents of the stream
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.copy(request.getInputStream(),out);
log.info("Request body :{}", out.toString(request.getCharacterEncoding()));
Map<String, String[]> parameterMap = request.getParameterMap();
log.info("Request parameters :{}",JSON.toJSONString(parameterMap)); filterChain.doFilter(request,response); }}Copy the code
Adjusting the timing of the getInputStream and getParameterMap methods resulted in two different results, which made me think it was a BUG. If we call getInputStream first, this will leave the parameters of getParameterMap unparsed. The following code is the Tomcat implementation embedded in SpringBoot.
Org. Apache. Catalina. Connector. Request:
protected void parseParameters(a) {
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
Charset charset = getCharset();
boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
parameters.setCharset(charset);
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}
// Note: If ! useBodyEncodingForURI, the query string encoding is
// that set towards the start of CoyoyeAdapter.service()
parameters.handleQueryParameters();
if (usingInputStream || usingReader) {
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( !getConnector().isParseBodyMethod(getMethod()) ) {
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();
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
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) { parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE);return; }}catch (IOException e) {
// Client disconnect
Context context = getContext();
if(context ! =null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"), e);
}
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
return;
}
parameters.processParameters(formData, 0, len);
} else if ("chunked".equalsIgnoreCase(
coyoteRequest.getHeader("transfer-encoding"))) {
byte[] formData = null;
try {
formData = readChunkedPostBody();
} catch (IllegalStateException ise) {
// chunkedPostTooLarge error
parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);
Context context = getContext();
if(context ! =null && context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),
ise);
}
return;
} catch (IOException e) {
// Client disconnect
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT);
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.setParseFailedReason(FailReason.UNKNOWN); }}}Copy the code
The above code is used to parse parameters, as can be seen from the name of the method. There is a key piece of information as follows:
if (usingInputStream || usingReader) {
success = true;
return;
}
Copy the code
This means that if usingInputStream or usingReader is true, parsing will be interrupted and the parsing will be considered successful. GetInputStream = getReader; getInputStream = getReader; getInputStream = getReader;
getInputStream()
public ServletInputStream getInputStream(a) throws IOException {
if (usingReader) {
throw new IllegalStateException(sm.getString("coyoteRequest.getInputStream.ise"));
}
// Set usingInputStream to true
usingInputStream = true;
if (inputStream == null) {
inputStream = new CoyoteInputStream(inputBuffer);
}
return inputStream;
}
Copy the code
getReader()
public BufferedReader getReader(a) throws IOException {
if (usingInputStream) {
throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
}
if (coyoteRequest.getCharacterEncoding() == null) {
// Nothing currently set explicitly.
// Check the content
Context context = getContext();
if(context ! =null) {
String enc = context.getRequestCharacterEncoding();
if(enc ! =null) {
// Explicitly set the context default so it is visible to
// InputBuffer when creating the Reader.setCharacterEncoding(enc); }}}// Set usingReader to true
usingReader = true;
inputBuffer.checkConverter();
if (reader == null) {
reader = new CoyoteReader(inputBuffer);
}
return reader;
}
Copy the code
Why is this implemented in Tomcat? As a Servlet container, it must be implemented in accordance with the Servlet specification. By searching the relevant documentation, we can find the contents of the Servlet specification. Here is a part of the Servlet3.1 specification about parameter parsing:
conclusion
The core problem we need to solve in order to get the parameters in the request is to make the stream repeatable, and note that reading the stream first will cause the parameters of getParameterMap to fail to be resolved.
The resources
-
www.cnblogs.com/alter888/p/…
-
www.iteye.com/blog/zhangb…