一個系統上線,肯定會或多或少的存在異常情況。為了更快更好的排雷,記錄請求參數和響應結果是非常必要的。所以,Nginx 和 Tomcat 之類的 web 服務器,都提供了訪問日志,可以幫助我們記錄一些請求信息。
本文是在我們的應用中,定義一個Filter
來實現記錄請求參數和響應結果的功能。
有一定經驗的都知道,如果我們在Filter
中讀取了HttpServletRequest
或者HttpServletResponse
的流,就沒有辦法再次讀取了,這樣就會造成請求異常。所以,我們需要借助 Spring 提供的ContentCachingRequestWrapper
和ContentCachingRequestWrapper
實現數據流的重復讀取。
定義 Filter
通常來說,我們自定義的Filter
是實現Filter
接口,然后寫一些邏輯,但是既然是在 Spring 中,那就借助 Spring 的一些特性。在我們的實現中,要繼承OncePerRequestFilter
實現我們的自定義實現。
從類名上推斷,OncePerRequestFilter
是每次請求只執行一次,但是,難道Filter
在一次請求中還會執行多次嗎?Spring 官方也是給出定義這個類的原因:
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 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.
也就是說,Spring 是為了兼容不同的 Web 容器,所以定義了只會執行一次的OncePerRequestFilter
。
接下來開始定義我們的Filter
類:
public class AccessLogFilter extends OncePerRequestFilter { //... 這里有一些必要的屬性 @Override protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException { // 如果是被排除的 uri,不記錄 access_log 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 boolean isFirstRequest = !isAsyncDispatch(request); final boolean shouldWrapRequest = isFirstRequest && !(request instanceof ContentCachingRequestWrapper) && shouldWrapMethod; final HttpServletRequest requestToUse = shouldWrapRequest ? new ContentCachingRequestWrapper(request) : request; final boolean shouldWrapResponse = !(response instanceof 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); } } // ... 這里是一些必要的方法
這段代碼就是整個邏輯的核心所在,其他的內容從源碼中找到。
分析
這個代碼中,整體的邏輯沒有特別復雜的地方,只需要注意幾個關鍵點就可以了。
-
默認的
HttpServletRequest
和HttpServletResponse
中的流被讀取一次之后,再次讀取會失敗,所以要使用ContentCachingRequestWrapper
和ContentCachingResponseWrapper
進行包裝,實現重復讀取。 -
既然我們可以自定義
Filter
,那我們依賴的組件中也可能會自定義Filter
,更有可能已經對請求和響應對象進行過封裝,所以,一定要先進行一步判斷。也就是request instanceof ContentCachingRequestWrapper
和response instanceof ContentCachingResponseWrapper
。
只要注意了這兩點,剩下的都是這個邏輯的細化實現。
運行
接下來我們就運行一遍,看看結果。先定義幾種不同的請求:普通 get 請求、普通 post 請求、上傳文件、下載文件,這四個接口幾乎可以覆蓋絕大部分場景。(因為都是比較簡單的寫法,源碼就不贅述了,可以從文末的源碼中找到)
先啟動項目,然后借助 IDEA 的 http 請求工具:
###普通 get 請求 GET http://localhost:8080/index/get?name=howard ###普通 post 請求 POST http://localhost:8080/index/post Content-Type: application/json {"name":"howard"} ###上傳文件 POST http://localhost:8080/index/upload 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-- ###下載文件 GET http://localhost:8080/index/download
再看看打印的日志:
2021-04-29 19:44:57.495 INFO 83448 --- [nio-8080-exec-1] c.h.d.s.filter.AccessLogFilter : time=44ms,ip=127.0.0.1,uri=/index/get,headers=[host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],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=36ms,ip=127.0.0.1,uri=/index/post,headers=[content-type:application/json,content-length:17,host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=application/json,responseContentType=application/json,params=,request={"name":"howard"},response={"name":"howard","timestamp":"1619696697540"}
2021-04-29 19:44:57.585 INFO 83448 --- [nio-8080-exec-3] c.h.d.s.filter.AccessLogFilter : time=20ms,ip=127.0.0.1,uri=/index/upload,headers=[content-type:multipart/form-data; boundary=WebAppBoundary,content-length:232,host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=multipart/form-data; boundary=WebAppBoundary,responseContentType=application/json,params=,request=,response={"contentLength":"0","contentType":"multipart/form-data"}
2021-04-29 19:44:57.626 INFO 83448 --- [nio-8080-exec-4] c.h.d.s.filter.AccessLogFilter : time=27ms,ip=127.0.0.1,uri=/index/download,headers=[host:localhost:8080,connection:Keep-Alive,user-agent:Apache-HttpClient/4.5.12 (Java/11.0.7),accept-encoding:gzip,deflate],status=200,requestContentType=null,responseContentType=application/octet-stream;charset=utf-8,params=,request=,response=
文末總結
自定義Filter
是比較簡單的,只要能夠注意幾個關鍵點就可以了。不過后續還有擴展的空間,比如:
-
定義排除的請求 uri,可以借助
AntPathMatcher
實現 ant 風格的定義 - 將請求日志單獨存放,可以借助 logback 或者 log4j2 等框架的的日志配置實現,這樣能更加方便的查找日志
- 與調用鏈技術結合,在請求日志中增加調用鏈的 TraceId 等,可以快速定位待查詢的請求日志
源碼
附上源碼:https://github.com/howardliu-cn/effective-spring/tree/main/spring-filter
到此這篇關于SpringBoot之自定義Filter獲取請求參數與響應結果案例詳解的文章就介紹到這了,更多相關SpringBoot之自定義Filter獲取請求參數與響應結果內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://www.howardliu.cn/spring-request-recorder/