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…