過濾器攔截器攔截了request後,controller的@RequestBody 無法獲取request內容,報錯 Required request body is missing 的根源

思凡念真發表於2024-10-15

SpringMVC的攔截器、過濾器、Controller之間的關係


眾所周知所有的post請求中的body引數是已流形式存在的,而流資料只能讀取一次(為啥看這裡),如果在攔截器和過濾器中需要對post引數進行處理的話,就會報Required request body is missing 異常。既然知道原因,那隻要能將流儲存起來就可以解決問題。

怎樣讓引數流能多次讀取? 我在網上找到的方案是使用HttpServletRequestWrapper包裝HttpServletRequest。


但是實際使用,對於controller引數中使用@RequestBody,仍舊會導致相同的問題。

類似於

而他的快取(caching)性其實表現在這裡,舉個例子

這裡getContentAsByteArray就是可以重複讀取的一個方法,其底層就是ByteArrayOutputStream

既然它可以被重複讀取那麼為什麼又會因為無法重複讀取而丟擲異常呢?

為什麼ConContentCachingRequestWrapper無法解決的重複讀取問題

我們先故意觸發這個異常,看看異常堆疊資訊

再轉到對應方法

對於我們這個需要反序列化的引數,含有RequestBody註解,且使用required的預設值true,且不為optional,所以這個判斷函式為true,所以丟擲這個異常的原因在於arg為null,進而問題出在readWithMessageConverters方法上。

那麼我們再去尋找什麼情況下這個方法會返回null(其實不是這裡返回的null,這裡是啟發我向上找的原因)

那麼我們就把斷點放到這裡(有註釋的AbstractMessageConverterMethodArgumentResolver類202行),再此執行

發現其實它比較的是內部的body是否為空,我們再來看這個EmptyBodyCheckingHttpInputMessage類到底在哪裡初始化的body這個變數

原來是在建構函式里面,我們再在藍色高亮處打個斷點再重新試試

結合idea提示的型別資訊和原始碼,也就說如果body不為空,那麼其中含有的inputstream類就要支援(mark,reset)或者還未讀取完畢。

我們再回看ContentCachingRequestWrapper這個類中的ContentCachingInputStream類,首先這個時候因為我們故意在攔截器消費了這個流,所以我們要看看它支不支援(mark,reset)功能

所以說不支援

那麼我們再看else分支的這個PushbackInputStream和他的read方法到底何方神聖

因為單引數初始化的後的pos =1 buf陣列長度 =1,即返回值為super.read()的返回值

即傳入的那個inputStream呼叫read()方法

結論

你看問題就出在這裡。還是呼叫的ServletInputStream的read,因為直接原請求流被我們消費了,所以返回值為-1

再走到了else中進行處理空body

在這個方法中返回了null(因為第一個引數為null),這就是為什麼這個方法返回為null的真正原因

進而我們上面提到的這個if為true的,也就因此丟擲了這個我們熟悉的

Required request body is missing異常提示

歸根結底是因為ContentCachingRequestWrapper的內部類 ContentCachingInputStream的read方法還是由ServletInputStream去執行read方法的

解決方案

我來提供一個簡單的解決方法

我們先來複習一下我們需要什麼樣的InputStream?支援reset,mark

那麼jdk有沒有這樣一個呢?有!ByteArrayInputStream,這個是個實現InputStream的假裝成流的字元陣列快取。

設計思路如下,由這個包裝類先行消費輸入流做成位元陣列儲存起來,透過getInputStream提供一個

ServletInputStream的實現類用於代理ByteArrayInputStream進行操作

ByteArrayInputStream只是保留了一個引用,同時這個body的字元陣列是隻讀的,也不用擔心執行緒安全問題,更不用擔心ByteArrayInputStream的關閉問題(畢竟不是真正的流)

import lombok.SneakyThrows;
import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

public class RepeatedlyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 快取下來的HTTP body
     */
    private byte[] body;
    private Charset charset;

    @SneakyThrows
    public RepeatedlyHttpServletRequestWrapper(HttpServletRequest request,Charset charset) {
        super(request);
        try {
            body = StreamUtils.copyToByteArray(request.getInputStream());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        this.charset = charset;
    }

    public RepeatedlyHttpServletRequestWrapper(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("不支援監聽");
        }

        @Override
        public int read() throws IOException {
            return byteArrayInputStream.read();
        }

        @Override
        public boolean markSupported() {
            return byteArrayInputStream.markSupported();
        }
    }

}

我們再來請求一次

這次可以了

@RequestMapping(value = "/api/test",method = { RequestMethod.POST, RequestMethod.GET })
    public Object test(HttpServletRequest request,@RequestBody Param param){
       System.out.println(parm.getName); 
}

參考:

https://juejin.cn/post/6858645006635401224#%E7%BB%93%E8%AE%BA

https://www.cnblogs.com/qixingchao/p/18262499

https://www.jianshu.com/p/9d3e9b92d535

相關文章