對于開發(fā)中我們在過濾器或者攔截器中往往會對請求體做驗證,往往會導(dǎo)致發(fā)生Required request body is missing的問題 例如 那么,在controller就無法取到這個的值而是直接拋出 有的博客會提出使用ConContentCachingRequestWrapper包裝一下ServletRequest來解決,但是實際使用,對于controller參數(shù)中使用@RequestBody,仍舊會導(dǎo)致相同的問題。 類似于 而他的緩存(caching)性其實表現(xiàn)在這里,舉個例子 這里getContentAsByteArray就是可以重復(fù)讀取的一個方法,其底層就是ByteArrayOutputStream 既然它可以被重復(fù)讀取那么為什么又會因為無法重復(fù)讀取而拋出異常呢? 為什么ConContentCachingRequestWrapper無法解決的重復(fù)讀取問題我們先故意觸發(fā)這個異常,看看異常堆棧信息 再轉(zhuǎn)到對應(yīng)方法 對于我們這個需要反序列化的參數(shù),含有RequestBody注解,且使用required的默認(rèn)值true,且不為optional,所以這個判斷函數(shù)為true,所以拋出這個異常的原因在于arg為null,進(jìn)而問題出在readWithMessageConverters方法上。 那么我們再去尋找什么情況下這個方法會返回null(其實不是這里返回的null,這里是啟發(fā)我向上找的原因) 我們再向上尋找body的賦值,同時發(fā)現(xiàn)上面也有個一個對messag.hasBody()的判定 那么我們就把斷點(diǎn)放到這里(有注釋的AbstractMessageConverterMethodArgumentResolver類202行),再此執(zhí)行 發(fā)現(xiàn)其實它比較的是內(nèi)部的body是否為空,我們再來看這個EmptyBodyCheckingHttpInputMessage類到底在哪里初始化的body這個變量 原來是在構(gòu)造函數(shù)里面,我們再在藍(lán)色高亮處打個斷點(diǎn)再重新試試 結(jié)合idea提示的類型信息和源碼,也就說如果body不為空,那么其中含有的inputstream類就要支持(mark,reset)或者還未讀取完畢。 我們再回看ContentCachingRequestWrapper這個類中的ContentCachingInputStream類,首先這個時候因為我們故意在攔截器消費(fèi)了這個流,所以我們要看看它支不支持(mark,reset)功能 所以說不支持 那么我們再看else分支的這個PushbackInputStream和他的read方法到底何方神圣 因為單參數(shù)初始化的后的pos =1 buf數(shù)組長度 =1,即返回值為super.read()的返回值 即傳入的那個inputStream調(diào)用read()方法 結(jié)論 你看問題就出在這里。還是調(diào)用的ServletInputStream的read,因為直接原請求流被我們消費(fèi)了,所以返回值為-1 再走到了else中進(jìn)行處理空body 在這個方法中返回了null(因為第一個參數(shù)為null),這就是為什么這個方法返回為null的真正原因 進(jìn)而我們上面提到的這個if為true的,也就因此拋出了這個我們熟悉的 Required request body is missing異常提示 歸根結(jié)底是因為ContentCachingRequestWrapper的內(nèi)部類 ContentCachingInputStream的read方法還是由ServletInputStream去執(zhí)行read方法的 解決方案我來提供一個簡單的解決方法 我們先來復(fù)習(xí)一下我們需要什么樣的InputStream?支持reset,mark 那么jdk有沒有這樣一個呢?有!ByteArrayInputStream,這個是個實現(xiàn)InputStream的假裝成流的字符數(shù)組緩存。 設(shè)計思路如下,由這個包裝類先行消費(fèi)輸入流做成比特數(shù)組儲存起來,通過getInputStream提供一個 ServletInputStream的實現(xiàn)類用于代理ByteArrayInputStream進(jìn)行操作 ByteArrayInputStream只是保留了一個引用,同時這個body的字符數(shù)組是只讀的,也不用擔(dān)心線程安全問題,更不用擔(dān)心ByteArrayInputStream的關(guān)閉問題(畢竟不是真正的流) public class RepeatableRequestWrapper extends HttpServletRequestWrapper { private byte[] body; private Charset charset; @SneakyThrows public RepeatableRequestWrapper(HttpServletRequest request, Charset charset) { super(request); body = request.getInputStream().readAllBytes(); this.charset = charset; } public RepeatableRequestWrapper(HttpServletRequest request) { this(request,StandardCharsets.UTF_8); } @Override public ServletInputStream getInputStream() throws IOException { return new RepeatableInputStream(); } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(getInputStream(),charset)); } private class RepeatableInputStream extends ServletInputStream{ private ByteArrayInputStream byteArrayInputStream; @Override public synchronized void reset() throws IOException { byteArrayInputStream.reset(); } @Override public synchronized void mark(int readlimit) { byteArrayInputStream.mark(readlimit); } public RepeatableInputStream() { byteArrayInputStream = new ByteArrayInputStream(body); } @Override public boolean isFinished() { return byteArrayInputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener readListener) { throw new UnsupportedOperationException("不支持監(jiān)聽"); } @Override public int read() throws IOException { return byteArrayInputStream.read(); } @Override public boolean markSupported() { return byteArrayInputStream.markSupported(); } } } 復(fù)制代碼 我們再來請求一次 舒服了,這次可以了 分類 |
|