是的,在預設情況下,一旦 ServletRequest
的輸入流(InputStream
或 Reader
)被讀取,流就被標記為已消費,資料也無法再次讀取。這是因為 ServletRequest
的輸入流基於 HTTP 請求的位元組流實現,讀取資料後,流會關閉或標記為已消費狀態,從而阻止重複讀取。
如何解決無法重複讀取流的問題
在某些情況下,我們可能需要在多個地方處理請求資料(如身份驗證、日誌記錄或業務邏輯等),那麼可以透過以下幾種方式來解決流不能重複讀取的問題:
1. 使用 HttpServletRequestWrapper
快取請求資料
可以建立一個自定義的 HttpServletRequestWrapper
,在請求第一次被讀取時快取流中的資料,以便後續可以多次讀取。
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {
private byte[] cachedBody;
public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
InputStream requestInputStream = request.getInputStream();
this.cachedBody = requestInputStream.readAllBytes();
}
@Override
public ServletInputStream getInputStream() {
return new CachedBodyServletInputStream(cachedBody);
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
private static class CachedBodyServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public CachedBodyServletInputStream(byte[] cachedBody) {
this.buffer = new ByteArrayInputStream(cachedBody);
}
@Override
public int read() {
return buffer.read();
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new UnsupportedOperationException();
}
}
}
在這個自定義的 HttpServletRequestWrapper
中,流第一次被讀取後快取到 cachedBody
位元組陣列,後續可以多次讀取。使用時,在 Filter
或 Servlet
中用包裝後的 HttpServletRequest
替換原始請求物件。
2. 在過濾器中替換 HttpServletRequest
可以在過濾器中將原始請求替換為 CachedBodyHttpServletRequest
例項,以便後續在 Servlet
中可以重複讀取流。
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class RequestBodyCachingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
HttpServletRequest cachedRequest = new CachedBodyHttpServletRequest((HttpServletRequest) request);
chain.doFilter(cachedRequest, response);
} else {
chain.doFilter(request, response);
}
}
}
總結
- 預設情況下,
ServletRequest
的輸入流讀取後便不可再讀。 - 可以使用
HttpServletRequestWrapper
來快取請求體內容。 - 使用快取後的請求物件即可實現流的重複讀取。
在 Java Web 應用中,Filter
中讀取 HttpServletRequest
的輸入流通常會導致流被“消費”,從而在後續的 Servlet
中無法再次讀取。為了使 HttpServletRequest
在 Filter
和 Servlet
中都能夠讀取,通常使用快取技術,即透過將請求資料儲存在記憶體中來實現流的重複讀取。這是因為 HttpServletRequest
預設的流是一次性讀取的,原始資料被消費後便不能再次獲取。
如果在 Filter
中讀取 HttpServletRequest
後在 Servlet
中仍然可以讀取流,通常是以下幾種情況之一:
1. 使用了包裝類(HttpServletRequestWrapper
)快取請求資料
這是最常用的解決方案。透過自定義一個 HttpServletRequestWrapper
,在流被讀取時,將資料快取為位元組陣列。這樣一來,每次請求讀取時都可以從快取中返回新的流,避免了流被“消費”後無法再次讀取的問題。
實現步驟大致如下:
- 在
Filter
中建立包裝類的例項。 - 快取請求體,並將包裝類傳遞到後續的
FilterChain
中。 - 在
Servlet
中,包裝類的getInputStream()
或getReader()
方法會返回新的流物件,從而實現流的重複讀取。
示例包裝類程式碼見前面的回答。
2. 使用了 Spring 框架
在 Spring MVC 中,RequestBody
也可以透過 Filter
和 Servlet
重複讀取。Spring 內部使用了 ContentCachingRequestWrapper
來包裝 HttpServletRequest
,其中的請求體被快取到位元組陣列,以實現流的重複讀取。例如,Spring 的 OncePerRequestFilter
和其他一些過濾器會自動包裝 HttpServletRequest
。
要啟用 ContentCachingRequestWrapper
,可以在專案中引入 Spring MVC,然後在 Filter
中手動建立或讓 Spring 自動管理。例如:
import org.springframework.web.util.ContentCachingRequestWrapper;
public class MyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
filterChain.doFilter(wrappedRequest, response);
// 讀取請求體
byte[] requestBody = wrappedRequest.getContentAsByteArray();
// 進一步處理 requestBody
}
}
3. 使用支援多次讀取的 Servlet 容器
某些 Servlet 容器在實現時支援多次讀取請求流,但這是容器實現的特性,通常不能依賴容器支援來解決該問題。例如,某些定製化的 Servlet 容器會自動快取請求體,但這不適用於所有環境,也不適用於所有容器。
總結
- 預設情況下,讀取
HttpServletRequest
的輸入流是一次性的。 - 可以透過
HttpServletRequestWrapper
來快取請求體,從而在Filter
和Servlet
中重複讀取流。 - Spring MVC 的
ContentCachingRequestWrapper
是實現流重複讀取的常用方案。