ServletRequest一旦讀取了流,流就關閉了,流中的資料一旦被消費就不能再次從流中讀取了,Servlet 是如何還能夠從ServletRequest流中讀取資料的

gongchengship發表於2024-10-28

是的,在預設情況下,一旦 ServletRequest 的輸入流(InputStreamReader)被讀取,流就被標記為已消費,資料也無法再次讀取。這是因為 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 位元組陣列,後續可以多次讀取。使用時,在 FilterServlet 中用包裝後的 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 中無法再次讀取。為了使 HttpServletRequestFilterServlet 中都能夠讀取,通常使用快取技術,即透過將請求資料儲存在記憶體中來實現流的重複讀取。這是因為 HttpServletRequest 預設的流是一次性讀取的,原始資料被消費後便不能再次獲取。

如果在 Filter 中讀取 HttpServletRequest 後在 Servlet 中仍然可以讀取流,通常是以下幾種情況之一:

1. 使用了包裝類(HttpServletRequestWrapper)快取請求資料

這是最常用的解決方案。透過自定義一個 HttpServletRequestWrapper,在流被讀取時,將資料快取為位元組陣列。這樣一來,每次請求讀取時都可以從快取中返回新的流,避免了流被“消費”後無法再次讀取的問題。

實現步驟大致如下:

  • Filter 中建立包裝類的例項。
  • 快取請求體,並將包裝類傳遞到後續的 FilterChain 中。
  • Servlet 中,包裝類的 getInputStream()getReader() 方法會返回新的流物件,從而實現流的重複讀取。

示例包裝類程式碼見前面的回答。

2. 使用了 Spring 框架

在 Spring MVC 中,RequestBody 也可以透過 FilterServlet 重複讀取。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 來快取請求體,從而在 FilterServlet 中重複讀取流。
  • Spring MVC 的 ContentCachingRequestWrapper 是實現流重複讀取的常用方案。

相關文章