1. 簡介
在本教程中,我們將學習如何使用 Spring 從 HttpServletRequest 多個次讀取 body。
HttpServletRequest 是一個接口,它暴露了 getInputStream 方法用於讀取 body。 默認情況下,InputStream 中的數據只能讀取一次。
2. Maven 依賴
首先我們需要合適的 spring-webmvc 和 jakarta.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 類的對象。
最後,我們創建了一個過濾器,將請求包裝器對象傳遞到過濾器鏈。因此,我們能夠多次讀取請求。