SpringBoot之自定義Filter獲取請(qǐng)求參數(shù)與響應(yīng)結(jié)果案例詳解
一個(gè)系統(tǒng)上線,肯定會(huì)或多或少的存在異常情況。為了更快更好的排雷,記錄請(qǐng)求參數(shù)和響應(yīng)結(jié)果是非常必要的。所以,Nginx 和 Tomcat 之類的 web 服務(wù)器,都提供了訪問日志,可以幫助我們記錄一些請(qǐng)求信息。
本文是在我們的應(yīng)用中,定義一個(gè)Filter
來實(shí)現(xiàn)記錄請(qǐng)求參數(shù)和響應(yīng)結(jié)果的功能。
有一定經(jīng)驗(yàn)的都知道,如果我們?cè)?code>Filter中讀取了HttpServletRequest
或者HttpServletResponse
的流,就沒有辦法再次讀取了,這樣就會(huì)造成請(qǐng)求異常。所以,我們需要借助 Spring 提供的ContentCachingRequestWrapper
和ContentCachingRequestWrapper
實(shí)現(xiàn)數(shù)據(jù)流的重復(fù)讀取。
定義 Filter
通常來說,我們自定義的Filter
是實(shí)現(xiàn)Filter
接口,然后寫一些邏輯,但是既然是在 Spring 中,那就借助 Spring 的一些特性。在我們的實(shí)現(xiàn)中,要繼承OncePerRequestFilter
實(shí)現(xiàn)我們的自定義實(shí)現(xiàn)。
從類名上推斷,OncePerRequestFilter
是每次請(qǐng)求只執(zhí)行一次,但是,難道Filter
在一次請(qǐng)求中還會(huì)執(zhí)行多次嗎?Spring 官方也是給出定義這個(gè)類的原因:
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 容器,所以定義了只會(huì)執(zhí)行一次的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); } } // ... 這里是一些必要的方法
這段代碼就是整個(gè)邏輯的核心所在,其他的內(nèi)容從源碼中找到。
分析
這個(gè)代碼中,整體的邏輯沒有特別復(fù)雜的地方,只需要注意幾個(gè)關(guān)鍵點(diǎn)就可以了。
- 默認(rèn)的
HttpServletRequest
和HttpServletResponse
中的流被讀取一次之后,再次讀取會(huì)失敗,所以要使用ContentCachingRequestWrapper
和ContentCachingResponseWrapper
進(jìn)行包裝,實(shí)現(xiàn)重復(fù)讀取。 - 既然我們可以自定義
Filter
,那我們依賴的組件中也可能會(huì)自定義Filter
,更有可能已經(jīng)對(duì)請(qǐng)求和響應(yīng)對(duì)象進(jìn)行過封裝,所以,一定要先進(jìn)行一步判斷。也就是request instanceof ContentCachingRequestWrapper
和response instanceof ContentCachingResponseWrapper
。
只要注意了這兩點(diǎn),剩下的都是這個(gè)邏輯的細(xì)化實(shí)現(xiàn)。
運(yùn)行
接下來我們就運(yùn)行一遍,看看結(jié)果。先定義幾種不同的請(qǐng)求:普通 get 請(qǐng)求、普通 post 請(qǐng)求、上傳文件、下載文件,這四個(gè)接口幾乎可以覆蓋絕大部分場景。(因?yàn)槎际潜容^簡單的寫法,源碼就不贅述了,可以從文末的源碼中找到)
先啟動(dòng)項(xiàng)目,然后借助 IDEA 的 http 請(qǐng)求工具:
###普通 get 請(qǐng)求 GET http://localhost:8080/index/get?name=howard ###普通 post 請(qǐng)求 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=
文末總結(jié)
自定義Filter
是比較簡單的,只要能夠注意幾個(gè)關(guān)鍵點(diǎn)就可以了。不過后續(xù)還有擴(kuò)展的空間,比如:
- 定義排除的請(qǐng)求 uri,可以借助
AntPathMatcher
實(shí)現(xiàn) ant 風(fēng)格的定義 - 將請(qǐng)求日志單獨(dú)存放,可以借助 logback 或者 log4j2 等框架的的日志配置實(shí)現(xiàn),這樣能更加方便的查找日志
- 與調(diào)用鏈技術(shù)結(jié)合,在請(qǐng)求日志中增加調(diào)用鏈的 TraceId 等,可以快速定位待查詢的請(qǐng)求日志
源碼
附上源碼:https://github.com/howardliu-cn/effective-spring/tree/main/spring-filter
推薦閱讀
- SpringBoot 實(shí)戰(zhàn):一招實(shí)現(xiàn)結(jié)果的優(yōu)雅響應(yīng)
- SpringBoot 實(shí)戰(zhàn):如何優(yōu)雅的處理異常
- SpringBoot 實(shí)戰(zhàn):通過 BeanPostProcessor 動(dòng)態(tài)注入 ID 生成器
- SpringBoot 實(shí)戰(zhàn):自定義 Filter 優(yōu)雅獲取請(qǐng)求參數(shù)和響應(yīng)結(jié)果
- SpringBoot 實(shí)戰(zhàn):優(yōu)雅的使用枚舉參數(shù)
- SpringBoot 實(shí)戰(zhàn):優(yōu)雅的使用枚舉參數(shù)(原理篇)
- SpringBoot 實(shí)戰(zhàn):在 RequestBody 中優(yōu)雅的使用枚舉參數(shù)
到此這篇關(guān)于SpringBoot之自定義Filter獲取請(qǐng)求參數(shù)與響應(yīng)結(jié)果案例詳解的文章就介紹到這了,更多相關(guān)SpringBoot之自定義Filter獲取請(qǐng)求參數(shù)與響應(yīng)結(jié)果內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot3 響應(yīng)式網(wǎng)絡(luò)請(qǐng)求客戶端的實(shí)現(xiàn)
- SpringBoot接收與響應(yīng)xml報(bào)文請(qǐng)求的實(shí)現(xiàn)
- springboot如何實(shí)現(xiàn)異步響應(yīng)請(qǐng)求(前端請(qǐng)求超時(shí)的問題解決)
- SpringBoot請(qǐng)求發(fā)送與信息響應(yīng)匹配實(shí)現(xiàn)方法介紹
- springboot vue完成發(fā)送接口請(qǐng)求顯示響應(yīng)頭信息
- SpringBoot請(qǐng)求響應(yīng)方式示例詳解
相關(guān)文章
Mybatis-Plus中update()和updateById()將字段更新為null
本文主要介紹了Mybatis-Plus中update()和updateById()將字段更新為null,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08RestTemplate響應(yīng)中如何獲取輸入流InputStream
這篇文章主要介紹了RestTemplate響應(yīng)中如何獲取輸入流InputStream問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-01-01SpringBoot加載不出來application.yml文件的解決方法
這篇文章主要介紹了SpringBoot加載不出來application.yml文件的解決方法,文中通過示例代碼講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作有一定的幫助,需要的朋友跟著小編來一起來學(xué)習(xí)吧2023-12-12SpringBoot創(chuàng)建maven多模塊項(xiàng)目實(shí)戰(zhàn)代碼
本篇文章主要介紹了SpringBoot創(chuàng)建maven多模塊項(xiàng)目實(shí)戰(zhàn)代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09