讀取 Spring 中的 HttpServletRequest 多次

Jackson,Spring MVC
Remote
0
10:12 PM · Nov 30 ,2025

1. 簡介

在本教程中,我們將學習如何使用 Spring 從 HttpServletRequest 多個次讀取 body。

HttpServletRequest 是一個接口,它暴露了 getInputStream 方法用於讀取 body。 默認情況下,InputStream 中的數據只能讀取一次。

2. Maven 依賴

首先我們需要合適的 spring-webmvcjakarta.servlet 依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>6.0.13</version>
</dependency>

另外,由於我們使用了 application/json content-type,因此需要 jackson-databind 依賴:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.17.2</version>
</dependency>

Spring 使用這個庫將數據轉換為和從 JSON 之間進行轉換。

3. Spring 的 ContentCachingRequestWrapper

Spring 提供了一個 ContentCachingRequestWrapper 類。該類提供了一個 getContentAsByteArray() 方法,用於多次讀取請求體.

不過,該類存在一個限制:我們不能使用 getInputStream()getReader() 方法多次讀取請求體。

該類通過消耗 InputStream 來緩存請求體。如果在一個過濾器中讀取 InputStream,則鏈式過濾器無法再次讀取它。由於這個限制,該類不適用於所有情況。

為了克服這個限制,讓我們現在看看更通用的解決方案。

4. 擴展 HttpServletRequest

讓我們創建一個新的類——CachedBodyHttpServletRequest,它繼承自 HttpServletRequestWrapper。 這樣,我們就不需要覆蓋 HttpServletRequest 接口的所有抽象方法。

HttpServletRequestWrapper 類有兩個抽象方法 getInputStream()getReader()。 我們將覆蓋這兩個方法並創建一個新的構造函數。

4.1. 構造函數

首先,讓我們創建一個構造函數。 在其中,我們將從實際的 InputStream 中讀取 body 並將其存儲在 byte[] 對象中:

public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {

    private byte[] cachedBody;

    public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
    }
}

結果,我們將能夠多次讀取 body。

4.2. getInputStream()

接下來,讓我們覆蓋 getInputStream() 方法。 我們將使用此方法讀取原始 body 並將其轉換為對象。

在該方法中,我們將創建一個並返回 CachedBodyServletInputStream 類(ServletInputStream 的實現)的新對象:

@Override
public ServletInputStream getInputStream() throws IOException {
    return new CachedBodyServletInputStream(this.cachedBody);
}

4.3. getReader()

然後,我們將覆蓋 getReader() 方法。 此方法返回一個 BufferedReader 對象:

@Override
public BufferedReader getReader() throws IOException {
    ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(this.cachedBody);
    return new BufferedReader(new InputStreamReader(byteArrayInputStream));
}

5. ServletInputStream 的實現

讓我們創建一個 類 – CachedBodyServletInputStream,它將實現 ServletInputStream

在這個類中,我們將創建一個新的構造函數,以及覆蓋 isFinished()isReady()read() 方法。

5.1. 構造函數

首先,讓我們創建一個新的構造函數,它接受一個字節數組。

在其中,我們將創建一個 新的 ByteArrayInputStream 實例,使用該字節數組。 然後,我們將將其分配給全局變量 cachedBodyInputStream

public class CachedBodyServletInputStream extends ServletInputStream {

    private InputStream cachedBodyInputStream;

    public CachedBodyServletInputStream(byte[] cachedBody) {
        this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);
    }
}

5.2. read()

然後,我們將覆蓋 read() 方法。

在這個方法中,我們將調用 ByteArrayInputStream#read:

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

5.3. isFinished()

然後,我們將覆蓋 isFinished() 方法。 此方法指示 InputStream 是否有更多數據可讀。 當可用零字節時,它返回 true

@Override
public boolean isFinished() {
    return cachedBody.available() == 0;
}

5.4. isReady()

同樣,我們將覆蓋 isReady() 方法。 此方法指示 InputStream 是否已準備好進行讀取。

由於我們已經將 InputStream 複製到字節數組中,因此我們將返回 true,以指示它始終可用:

@Override
public boolean isReady() {
    return true;
}

6. 過濾器

最後,我們創建一個新的過濾器,以利用 CachedBodyHttpServletRequest 類。在這裏,我們將擴展 Spring 的 OncePerRequestFilter 類。該類有一個抽象方法 doFilterInternal()

在該方法中,我們將 從實際請求對象創建一個 CachedBodyHttpServletRequest 類的對象

CachedBodyHttpServletRequest cachedBodyHttpServletRequest =
  new CachedBodyHttpServletRequest(request);

然後我們將 將此新的請求包裝對象傳遞到過濾器鏈。因此,所有後續對 getInputStream() 方法的調用都將調用重寫的該方法:

filterChain.doFilter(cachedContentHttpServletRequest, response);

7. 結論

在本教程中,我們快速介紹了 ContentCachingRequestWrapper 類。我們還看到了它的侷限性。

然後,我們創建了 HttpServletRequestWrapper 類的新的實現。我們重寫了 getInputStream() 方法以返回 ServletInputStream 類的對象。

最後,我們創建了一個過濾器,將請求包裝器對象傳遞到過濾器鏈。因此,我們能夠多次讀取請求。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.