解決 HttpServletRequest 的輸入流不能重複讀的問題

weiweiyi發表於2023-04-15

背景

寫程式碼的時候遇到 HttpServletRequest 裡的資料為null, 感覺很奇怪,同樣的請求,剛才還不為null。

image.png

排查

經過debug發現,HttpServletRequest 在 106行 之前有值, 但在106行之後為null。

image.png

而這個方法只是簡單地讀了一遍 httpServletRequest 裡面的資料

ServletInputStream inputStream = httpServletRequest.getInputStream()
StringBuilder sb = new StringBuilder();

try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()))) {
  String line;
  while ((line = reader.readLine()) != null) {
    sb.append(line);
  }
} catch (IOException e) {
  throw new RuntimeException(e);
}

return sb.toString();

於是猜想 HttpServletRequest裡的 inputStream 只能讀一次?

去查了一下果真如此。下面瞭解一下原因。

HttpServletRequest的輸入流只能讀取一次的原因

首先我們使用 httpServletRequest.getInputStream()

返回的是 ServletInputStream 類, 它繼承於 InputStream

image.png

而我們對這個流讀資料其實 呼叫了 InputStream的 read()方法。

image.png

讀取流的時候會根據position來獲取當前位置,每讀取一次,該標誌就會移動一次,如果讀到最後,read()返回-1,表示已經讀取完了。

while(inputStream.read(b)) != -1) {
  
}

如果想要重新讀取,可以呼叫 inputstream.reset() 方法,
image.png

但是能否 reset 取決於markSupported方法,返回true可以reset,反之則不行。

檢視 ServletInputStream 可知,這個類並沒有重寫markSupported和reset方法。

所以這就是 HttpServletRequest輸入流只能讀取一次原因

解決

解決的核心思路就是對HttpServletRequest輸入流進行備份。

將請求體中的流copy一份,重寫getInputStream()和getReader()方法

每次呼叫覆寫後的getInputStream()方法都是從複製出來的二進位制陣列中進行獲取。

RequestWrapper.class

/**
 * 解決request流只讀取一次的問題
 */
@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {

  /**
   * 儲存body資料的容器
   */
  private final byte[] body;

  public RequestWrapper(HttpServletRequest request) throws IOException {
    super(request);

    // 將body資料儲存起來
    body = getBodyString(request).getBytes(Charset.defaultCharset());
  }

  /**
   * 獲取請求Body
   *
   * @param request request
   * @return String
   */
  private String getBodyString(final ServletRequest request) {
    try {
      return inputStream2String(request.getInputStream());
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * 獲取請求Body
   *
   * @return String
   */
  public String getBodyString() {
    final InputStream inputStream = new ByteArrayInputStream(body);

    return inputStream2String(inputStream);
  }

  /**
   * 將inputStream裡的資料讀取出來並轉換成字串
   *
   * @param inputStream inputStream
   * @return String
   */
  private String inputStream2String(InputStream inputStream) {
    StringBuilder sb = new StringBuilder();

    try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()))) {
      String line;
      while ((line = reader.readLine()) != null) {
        sb.append(line);
      }
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    return sb.toString();
  }

  @Override
  public BufferedReader getReader() throws IOException {
    return new BufferedReader(new InputStreamReader(getInputStream()));
  }

  @Override
  public ServletInputStream getInputStream() throws IOException {

    final ByteArrayInputStream inputStream = new ByteArrayInputStream(body);

    return new ServletInputStream() {
      @Override
      public int read() throws IOException {
        return inputStream.read();
      }

      @Override
      public boolean isFinished() {
        return false;
      }

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

      @Override
      public void setReadListener(ReadListener readListener) {
      }
    };
  }

}

Filter

除了要寫一個包裝器外,我們還需要在過濾器裡將原生的 HttpServletRequest 物件替換成我們的RequestWrapper物件。

建立過濾器

/**
 * 解決request流只讀取一次的問題
 */
public class ReplaceStreamFilter implements Filter {

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    ServletRequest requestWrapper = new RequestWrapper((HttpServletRequest) request);
    chain.doFilter(requestWrapper, response);
  }
}

配置過濾器

@Configuration
class FilterConfig {
  /**
   * 註冊過濾器
   *
   * @return FilterRegistrationBean
   */
  @Bean
  public FilterRegistrationBean someFilterRegistration() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(replaceStreamFilter());
    registration.addUrlPatterns("/*");
    registration.setName("streamFilter");
    return registration;
  }

  /**
   * 例項化StreamFilter
   *
   * @return Filter
   */
  @Bean(name = "replaceStreamFilter")
  public Filter replaceStreamFilter() {
    return new ReplaceStreamFilter();
  }
}

使用

使用的時候用我們建立的 RequestWrapper 讀資料就好了

String requestBody =  new RequestWrapper(httpServletRequest).getBodyString();

相關文章