“This is the 24th day of my participation in the Gwen Challenge in November. Check out the details: The Last Gwen Challenge in 2021.”

Hello, I’m looking at the mountains.

A system on-line, certain meeting more or less existence abnormal circumstance. It is necessary to record request parameters and response results for faster and better mine clearance. So, Web servers such as Nginx and Tomcat provide access logs to help us log requests.

In our application, this paper defines a Filter to realize the function of recording request parameters and response results.

As any experienced reader knows, if we read an HttpServletRequest or HttpServletResponse stream in a Filter, there is no way to read it again and this will result in a request exception. So, we need to use the Spring ContentCachingRequestWrapper and ContentCachingRequestWrapper realize data stream reads.

Define the Filter

Normally, we would implement the Filter interface and then write some logic, but since we’re in Spring, we’ll use some of Spring’s features. In our implementation, we inherit OncePerRequestFilter to implement our custom implementation.

From the class name, OncePerRequestFilter is executed once per request, but can Filter be executed more than once in a single request? Spring officials also give reasons for defining this class:

Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, javax.servlet.FilterChain) method with HttpServletRequest and HttpServletResponse arguments.

As of the Servlet 3.0, a filter may be invoked as part of a REQUEST or ASYNC dispatches that occur in separate threads. A filter can be configured in web.xml whether it should be involved in async dispatches. However, in some cases servlet containers assume different default configuration. Therefore sub-classes can override the method shouldNotFilterAsyncDispatch() to declare statically if they should indeed be invoked, once, during both types of dispatches in order to provide thread initialization, logging, security, and so on. This mechanism complements and does not replace the need to configure a filter in web.xml with dispatcher types.

Subclasses may use isAsyncDispatch(HttpServletRequest) to determine when a filter is invoked as part of an async dispatch, and use isAsyncStarted(HttpServletRequest) to determine when the request has been placed in async mode and therefore the current dispatch won’t be the last one for the given request.

Yet another dispatch type that also occurs in its own thread is ERROR. Subclasses can override shouldNotFilterErrorDispatch() if they wish to declare statically if they should be invoked once during error dispatches.

That is, In order to be compatible with different Web containers, Spring defines OncePerRequestFilter that will only be executed once.

Let’s start defining our Filter class:

public class AccessLogFilter extends OncePerRequestFilter {
    / /... There are some necessary attributes

    @Override
    protected void doFilterInternal(final HttpServletRequest request,
                                    final HttpServletResponse response,
                                    final FilterChain filterChain)
            throws ServletException, IOException {
        // If the uri is excluded, access_log is not recorded
        if (matchExclude(request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }

        final String requestMethod = request.getMethod();
        final boolean shouldWrapMethod = StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.PUT.name())
                || StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.POST.name());

        final booleanisFirstRequest = ! isAsyncDispatch(request);final booleanshouldWrapRequest = isFirstRequest && ! (requestinstanceof ContentCachingRequestWrapper) && shouldWrapMethod;
        final HttpServletRequest requestToUse = shouldWrapRequest ? new ContentCachingRequestWrapper(request) : request;

        final booleanshouldWrapResponse = ! (responseinstanceof ContentCachingResponseWrapper) && shouldWrapMethod;
        final HttpServletResponse responseToUse = shouldWrapResponse ? new ContentCachingResponseWrapper(response) : response;

        final long startTime = System.currentTimeMillis();
        Throwable t = null;
        try {
            filterChain.doFilter(requestToUse, responseToUse);
        } catch (Exception e) {
            t = e;
            throw e;
        } finally{ doSaveAccessLog(requestToUse, responseToUse, System.currentTimeMillis() - startTime, t); }}/ /... Here are some of the necessary ways
Copy the code

This code is the heart of the whole logic, the rest is found in the source code.

Analysis of the

There is nothing particularly complicated about the overall logic in this code, just a few key points to pay attention to.

  1. The defaultHttpServletRequestandHttpServletResponseIf a stream is read once, it will fail to be read againContentCachingRequestWrapperandContentCachingResponseWrapperFor packaging, the realization of repeated reading.
  2. Now that we can customizeFilter, then we may also have customization in the components we depend onFilterIt is more likely that request and response objects have already been wrapped, so be sure to make a judgment call first. That isrequest instanceof ContentCachingRequestWrapperandresponse instanceof ContentCachingResponseWrapper.

With these two points in mind, all that remains is a refinement of the logic.

run

So let’s run it and see what happens. Define several different requests: normal GET request, normal POST request, upload file, download file, these four interfaces can cover almost any scenario. (because are relatively simple to write, the source code is not redundant, can be found from the end of the source code)

Start the project and then use IDEA’s HTTP request tool:

# # # ordinary get request get http://localhost:8080/index/get? Name = Howard # # # ordinary post request post http://localhost:8080/index/post the content-type: Application/json {" name ":" Howard "} # # # upload files POST http://localhost:8080/index/upload the content-type: multipart/form - data; boundary=WebAppBoundary --WebAppBoundary Content-Disposition: form-data; name="file"; filename="history.txt" Content-Type: Multipart/form - data < / Users/liuxh/history. TXT - WebAppBoundary - # # # download files GET http://localhost:8080/index/downloadCopy the code

Look again at the printed log:

The 19:44:57 2021-04-29. 83448-495 the INFO [nio - 8080 - exec - 1] C.H.D.S.F ilter. AccessLogFilter: Time = 44 ms, IP = 127.0.0.1, uri = / index/get, headers = [host: localhost: 8080, connection: Keep Alive, the user-agent: Apache HttpClient / 4. 5.12 (Java / 11.0.7), the accept - encoding: gzip, deflate], the status = 200, requestContentType = null, responseContentType = text/plain. Charset =UTF-8,params=name= Howard,request=, Response = 2021-04-29 19:44:57.551 INFO 83448 -- [NIO-8080-exec-2] c.h.d.s.filter.AccessLogFilter : Time = 36, ms IP = 127.0.0.1, uri = / index/post, headers = [the content-type: application/json, content - length: 17, host: localhost: 8080, conn Ection: Keep Alive, the user-agent: Apache HttpClient / 4.5.12 (Java / 11.0.7), the accept - encoding: gzip, deflate], the status = 200, requestContentType = application/json, responseContentType = applicati On/json, params =, request = {" name ":" Howard "}, the response = {" name ":" Howard ", "timestamp" : "1619696697540"} 19:44:57. 2021-04-29 585  INFO 83448 --- [nio-8080-exec-3] c.h.d.s.filter.AccessLogFilter : Time = 20 ms, IP = 127.0.0.1, uri = / index/upload, headers = [the content-type: multipart/form - data; A boundary = WebAppBoundary, content - length: 232, host: localhost: 8080, connection: Keep Alive, the user-agent: Apache HttpClient / 4.5.1 2 (Java / 11.0.7), the accept - encoding: gzip, deflate], the status = 200, requestContentType = multipart/form - data; boundary=WebAppBoundary,responseContentType=application/json,params=,request=,response={"contentLength":"0","contentType ":" multipart/form - the data "} 19:44:57 2021-04-29. 83448-626 the INFO [nio - 8080 - exec - 4] C.H.D.S.F ilter. AccessLogFilter: Time = 27 ms, IP = 127.0.0.1, uri = / index/download, headers = [host: localhost: 8080, connection: Keep Alive, the user-agent: Apache - HttpClie Nt / 4.5.12 (Java / 11.0.7), the accept - encoding: gzip, deflate], the status = 200, requestContentType = null, responseContentType = application/octet - STR eam; charset=utf-8,params=,request=,response=Copy the code

At the end of the article to summarize

Custom filters are relatively simple, as long as you pay attention to a few key points. But there is room for further expansion, such as:

  1. To define the excluded request URI, you can useAntPathMatcherImplement ant style definitions
  2. Request logs can be stored separately using logging configuration in frameworks such as Logback or log4j2 to make it easier to find logs
  3. Combined with the call chain technology, you can add the TraceId of the call chain to the request log to quickly locate the request log to be queried

You can pay attention to the public account “mountain hut”, reply to “Spring” to obtain the source code.

Recommended reading

  • SpringBoot: elegant response to achieve results in one move
  • SpringBoot: How to handle exceptions gracefully
  • SpringBoot: Dynamically injects the ID generator through the BeanPostProcessor
  • SpringBoot: Customizes Filter to gracefully obtain request parameters and response results
  • SpringBoot: Elegant use of enumeration parameters
  • SpringBoot: Elegant use of enumeration parameters (Principles)
  • SpringBoot: Gracefully use enumeration parameters in the RequestBody
  • SpringBoot: Gracefully using enumeration parameters in the RequestBody
  • Do unit tests with JUnit5+MockMvc+Mockito
  • SpringBoot: Loads and reads resource files

Hello, I’m looking at the mountains. Swim in the code, play to enjoy life. If this article is helpful to you, please like, bookmark, follow. Welcome to follow the public account “Mountain Hut”, discover a different world.