SpringBoot項(xiàng)目中HTTP請(qǐng)求體只能讀一次的解決方案
問(wèn)題描述
在基于Spring開(kāi)發(fā)Java項(xiàng)目時(shí),可能需要重復(fù)讀取HTTP請(qǐng)求體中的數(shù)據(jù),例如使用攔截器打印入?yún)⑿畔⒌?,但?dāng)我們重復(fù)調(diào)用getInputStream()或者getReader()時(shí),通常會(huì)遇到類(lèi)似以下的錯(cuò)誤信息:
大體的意思是當(dāng)前request的getInputStream()已經(jīng)被調(diào)用過(guò)了。那為什么會(huì)出現(xiàn)這個(gè)問(wèn)題呢?
原因分析
主要原因有兩個(gè),一是Java自身的設(shè)計(jì)中,InputStream作為數(shù)據(jù)管道本身只支持讀取一次,如果要支持重復(fù)讀取的話就需要重新初始化;二是Servlet容器中Request的實(shí)現(xiàn)問(wèn)題,我們以默認(rèn)的Tomcat為例,可以發(fā)現(xiàn)在Request有兩個(gè)boolean類(lèi)型的屬性,分別是usingReader和usingInputStream,當(dāng)調(diào)用getInputStream()或getReader()時(shí)會(huì)分別檢查兩個(gè)屬性的值,并在執(zhí)行后將對(duì)應(yīng)的屬性設(shè)置為true,如果在檢查時(shí)變量的值已經(jīng)為true了,那么就會(huì)報(bào)出以上錯(cuò)誤信息。
解決方案
不太可行的方案:簡(jiǎn)單粗暴的反射機(jī)制
涉及到變量的修改,我們首先想到的就是有沒(méi)有提供方法進(jìn)行修改,不過(guò)可惜的是usingReader和usingInputStream并未提供,所以想要在使用過(guò)程中修改這兩個(gè)屬性估計(jì)只能靠反射了,在使用過(guò)程中每次調(diào)用后通過(guò)反射將usingReader和usingInputStream設(shè)置為false,每次根據(jù)讀取出的內(nèi)容把數(shù)據(jù)流初始化回去,理論上就可以再次讀取了。
首先說(shuō)反射機(jī)制本身就是通過(guò)破壞類(lèi)的封裝來(lái)實(shí)現(xiàn)動(dòng)態(tài)修改的,有點(diǎn)過(guò)于粗暴了,其次也是主要原因,我們只能針對(duì)我們自己實(shí)現(xiàn)的代碼進(jìn)行處理,框架本身如果調(diào)用getInputStream()和getReader()的話,我們就沒(méi)法通過(guò)這個(gè)辦法干預(yù)了,所以這個(gè)方案在給予Spring的Web項(xiàng)目中并不可行。
理論上可行的方案:HttpServletRequest接口
HttpServletRequest是一個(gè)接口,理論上我們只需要?jiǎng)?chuàng)建一個(gè)實(shí)現(xiàn)類(lèi)就可以自定義getInputStream()和getReader()的行為,自然也就能解決RequestBody不能重復(fù)讀取的問(wèn)題,但這個(gè)方案的問(wèn)題在于HttpServletRequest有70個(gè)方法,而我們只需要修改其中兩個(gè)而已,通過(guò)這種方式去解決有點(diǎn)得不償失。
部分場(chǎng)景可行的方案:ContentCachingRequestWrapper
Spring本身提供了一個(gè)Request包裝類(lèi)來(lái)處理重復(fù)讀取的問(wèn)題,即ContentCachingRequestWrapper,其實(shí)現(xiàn)思路就是在讀取RequestBody時(shí)將內(nèi)存緩存到它內(nèi)部的一個(gè)字節(jié)流中,后續(xù)讀取可以通過(guò)調(diào)用getContentAsString()或getContentAsByteArray()獲取到緩存下來(lái)的內(nèi)容。
之所以說(shuō)這個(gè)方案是部分場(chǎng)景可行主要是兩個(gè)方面,一是ContentCachingRequestWrapper沒(méi)有重寫(xiě)getInputStream()和getReader()方法,所以框架中使用這兩個(gè)方法的地方依然獲取不到緩存下來(lái)的內(nèi)容,僅支持自定義的業(yè)務(wù)邏輯;第二點(diǎn)和第一點(diǎn)有所關(guān)聯(lián),因?yàn)槠錄](méi)有修改getInputStream()和getReader()方法,所以我們?cè)谑褂脮r(shí)只能在使用RequestBody注解后使用ContentCachingRequestWrapper,否則就會(huì)出現(xiàn)RequestBody注解修飾的參數(shù)無(wú)法正常讀取請(qǐng)求體的問(wèn)題,也就限定了它的使用范圍如下圖所示:
如果僅需要在業(yè)務(wù)代碼后再次讀取請(qǐng)求體內(nèi)容,那么使用ContentCachingRequestWrapper也足以滿(mǎn)足需求,具體使用方法請(qǐng)參考下一節(jié)的說(shuō)明。
目前的最佳實(shí)踐:繼承HttpServletRequestWrapper
之前我們提到實(shí)現(xiàn)HttpServletRequest需要實(shí)現(xiàn)70個(gè)方法,所以不太可能自行實(shí)現(xiàn),這個(gè)方案算是進(jìn)階版本,繼承HttpServletRequest的實(shí)現(xiàn)類(lèi),之后再自定義我們需要修改的兩個(gè)方法。
HttpServletRequest作為一個(gè)接口,肯定會(huì)有其實(shí)現(xiàn)去支撐它的業(yè)務(wù)功能,因?yàn)镾ervlet容器的選擇較多,我們也不能使用某一方提供的實(shí)現(xiàn),所以選擇的范圍也就被限制到了Java EE(現(xiàn)在叫Jakarta EE)標(biāo)準(zhǔn)范圍內(nèi),通過(guò)查看HttpServletRequest的實(shí)現(xiàn),可以發(fā)現(xiàn)在標(biāo)準(zhǔn)內(nèi)提供了一個(gè)包裝類(lèi):HttpServletRequestWrapper,我們的方案也是圍繞它展開(kāi)。
思路簡(jiǎn)述
- 自定義子類(lèi),繼承HttpServletRequestWrapper,在子類(lèi)的構(gòu)造方法中將RequestBody緩存到自定義的屬性中。
- 自定義getInputStream()和getReader()的業(yè)務(wù)邏輯,不再校驗(yàn)usingReader和usingInputStream,且在調(diào)用時(shí)讀取緩存下來(lái)的內(nèi)容。
- 自定義Filter,將默認(rèn)的HttpServletRequest替換為自定義的包裝類(lèi)。
代碼展示
- 繼承HttpServletRequestWrapper,實(shí)現(xiàn)子類(lèi)CustomRequestWrapper,并自定義getInputStream()和getReader()的業(yè)務(wù)邏輯
// 1.繼承HttpServletRequestWrapper public class CustomRequestWrapper extends HttpServletRequestWrapper { // 2.定義final屬性,用于緩存請(qǐng)求體內(nèi)容 private final byte[] content; public CustomRequestWrapper(HttpServletRequest request) throws IOException { super(request); // 3.構(gòu)造方法中將請(qǐng)求體內(nèi)容緩存到內(nèi)部屬性中 this.content = StreamUtils.copyToByteArray(request.getInputStream()); } // 4.重新getInputStream() @Override public ServletInputStream getInputStream() { // 5.將緩存下來(lái)的內(nèi)容轉(zhuǎn)換為字節(jié)流 final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener listener) { } @Override public int read() { // 6.讀取時(shí)讀取第5步初始化的字節(jié)流 return byteArrayInputStream.read(); } }; } // 7.重寫(xiě)getReader()方法,這里復(fù)用getInputStream()的邏輯 @Override public BufferedReader getReader() { return new BufferedReader(new InputStreamReader(getInputStream())); } }
- 自定義Filter將默認(rèn)的HttpServletRequest替換為自定義的CustomRequestWrapper
// 1.實(shí)現(xiàn)Filter接口,此處也可以選擇繼承HttpFilter public class RequestWrapperFilter implements Filter { // 2. 重寫(xiě)或?qū)崿F(xiàn)doFilter方法 @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 3.此處判斷是為了縮小影響范圍,本身CustomRequestWrapper只是針對(duì)HttpServletRequest,不進(jìn)行判斷可能會(huì)影響其他類(lèi)型的請(qǐng)求 if (request instanceof HttpServletRequest) { // 4.將默認(rèn)的HttpServletRequest轉(zhuǎn)換為自定義的CustomRequestWrapper CustomRequestWrapper requestWrapper = new CustomRequestWrapper((HttpServletRequest) request); // 5.將轉(zhuǎn)換后的request傳遞至調(diào)用鏈中 chain.doFilter(requestWrapper, response); } else { chain.doFilter(request, response); } } }
- 將Filter注冊(cè)到Spring容器,這一步可以通過(guò)多種方式執(zhí)行,這里采用比較傳統(tǒng)但比較靈活的Bean方式注冊(cè),如果圖方便可以通過(guò)ServletComponentScan注解+ WebFilter注解的方式。
/** * 過(guò)濾器配置,支持第三方過(guò)濾器 */ @Configuration public class FilterConfigure { /** * 請(qǐng)求體封裝 * @return */ @Bean public FilterRegistrationBean<RequestWrapperFilter> filterRegistrationBean(){ FilterRegistrationBean<RequestWrapperFilter> bean = new FilterRegistrationBean<>(); bean.setFilter(new RequestWrapperFilter()); bean.addUrlPatterns("/*"); return bean; } }
至此我們就可以在項(xiàng)目中重復(fù)讀取請(qǐng)求體了,如果選擇使用Spring提供的ContentCachingRequestWrapper,那么在Filter中將CustomRequestWrapper替換為ContentCachingRequestWrapper即可,不過(guò)需要注意在上一節(jié)提到的可用范圍較小的問(wèn)題。
以上就是SpringBoot項(xiàng)目中HTTP請(qǐng)求體只能讀一次的解決方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot HTTP請(qǐng)求只讀一次的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java?IO流之StringWriter和StringReader用法分析
這篇文章主要介紹了Java?IO流之StringWriter和StringReader用法分析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12Java EasyExcel實(shí)現(xiàn)導(dǎo)出多sheet并設(shè)置單元格樣式
EasyExcel是一個(gè)基于Java的、快速、簡(jiǎn)潔、解決大文件內(nèi)存溢出的Excel處理工具,下面我們就來(lái)學(xué)習(xí)一下EasyExcel如何實(shí)現(xiàn)導(dǎo)出多sheet并設(shè)置單元格樣式吧2023-11-11Java利用MYSQL LOAD DATA LOCAL INFILE實(shí)現(xiàn)大批量導(dǎo)入數(shù)據(jù)到MySQL
Mysql load data的使用,MySQL的LOAD DATAINFILE語(yǔ)句用于高速地從一個(gè)文本文件中讀取行,并裝入一個(gè)表中2018-03-03Maven將代碼及依賴(lài)打成一個(gè)Jar包的方式詳解(最新推薦)
這篇文章主要介紹了Maven將代碼及依賴(lài)打成一個(gè)Jar包的方式,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05idea項(xiàng)目的左側(cè)目錄沒(méi)了如何設(shè)置
這篇文章主要介紹了idea項(xiàng)目的左側(cè)目錄沒(méi)了如何設(shè)置的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02SpringSecurity?表單登錄的實(shí)現(xiàn)
本文主要介紹了SpringSecurity?表單登錄的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-12-12淺析Java設(shè)計(jì)模式編程中的單例模式和簡(jiǎn)單工廠模式
這篇文章主要介紹了淺析Java設(shè)計(jì)模式編程中的單例模式和簡(jiǎn)單工廠模式,使用設(shè)計(jì)模式編寫(xiě)代碼有利于團(tuán)隊(duì)協(xié)作時(shí)程序的維護(hù),需要的朋友可以參考下2016-01-01