springboot請求體中的流只能讀取一次的問題 httpServletRequest中的流只能讀取一次的原因 springboot-攔截器-過濾器-Required request body is missing 異常


場景交代

在springboot中添加攔截器進行權限攔截時,需要獲取請求參數進行驗證。當參數在url后面時(queryString)獲取參數進行驗證之后程序正常運行。但是,當請求參數在請求體中的時候,通過流的方式將請求體取出參數進行驗證之后,發現后續流程拋出錯誤:

  Required request body is missing ...

經過排查,發現ServletInputStream的流只能讀取一次(參考:httpServletRequest中的流只能讀取一次的原因)。

這就是為什么在攔截器中讀取消息體之后,controller的@RequestBody注解無法獲取參數的原因。

解決思路

既然知道了原因,那就可以想到一個大概思路了:可不可以把請求的body流換成可重復讀的流?

答案是可以的。可以通過繼承HttpServletRequestWrapper類進行。

解決方案

1. 繼承HttpServletRequestWrapper

繼承HttpServletRequestWrapper類,將請求體中的流copy一份出來,覆寫getInputStream()和getReader()方法供外部使用。如下,每次調用覆寫后的getInputStream()方法都是從復制出來的二進制數組中進行獲取,這個二進制數組在對象存在期間一直存在,這樣就實現了流的重復讀取。

  import java.io.BufferedReader;
  import java.io.ByteArrayInputStream;
  import java.io.IOException;
  import java.io.InputStreamReader;
   
  import javax.servlet.ReadListener;
  import javax.servlet.ServletInputStream;
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletRequestWrapper;
   
  import org.springframework.util.StreamUtils;
   
  /**
  *
  * 從請求體中獲取參數請求包裝類:<br>
  * @author nick
  * @version 5.0 since 2018年9月5日
  */
  public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{
   
  private byte[] requestBody = null;//用於將流保存下來
   
  public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
  super(request);
  requestBody = StreamUtils.copyToByteArray(request.getInputStream());
  }
   
  @Override
  public ServletInputStream getInputStream() throws IOException {
   
  final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
   
  return new ServletInputStream() {
   
  @Override
  public int read() throws IOException {
  return bais.read();
  }
   
  @Override
  public boolean isFinished() {
  return false;
  }
   
  @Override
  public boolean isReady() {
  return false;
  }
   
  @Override
  public void setReadListener(ReadListener readListener) {
   
  }
  };
  }
   
  @Override
  public BufferedReader getReader() throws IOException{
  return new BufferedReader(new InputStreamReader(getInputStream()));
  }
  }
   
2. 替換原始request對象

現在可重復讀取流的請求對象構造好了,但是需要在攔截器中獲取,就需要將包裝后的請求對象放在攔截器中。由於filter在interceptor之前執行,因此可以通過filter進行實現。

創建filer,在filter中對request對象用包裝后的request替換。

  import java.io.IOException;
   
  import javax.servlet.Filter;
  import javax.servlet.FilterChain;
  import javax.servlet.FilterConfig;
  import javax.servlet.ServletException;
  import javax.servlet.ServletRequest;
  import javax.servlet.ServletResponse;
  import javax.servlet.annotation.WebFilter;
  import javax.servlet.http.HttpServletRequest;
   
  import com.znz.dns.controller.interceptor.auth.BodyReaderHttpServletRequestWrapper;
   
  @WebFilter(filterName="bodyReaderFilter",urlPatterns="/*")
  public class BodyReaderFilter implements Filter{
   
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
  // do nothing
  }
   
  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  throws IOException, ServletException {
  ServletRequest requestWrapper=null;
  if(request instanceof HttpServletRequest) {
  requestWrapper=new BodyReaderHttpServletRequestWrapper((HttpServletRequest)request);
  }
  if(requestWrapper==null) {
  chain.doFilter(request, response);
  }else {
  chain.doFilter(requestWrapper, response);
  }
   
  }
   
  @Override
  public void destroy() {
  // do nothing
   
  }
   
  }
   

配置filter

  @Bean
  public FilterRegistrationBean<BodyReaderFilter> Filters() {
  FilterRegistrationBean<BodyReaderFilter> registrationBean = new FilterRegistrationBean<BodyReaderFilter>();
  registrationBean.setFilter(new BodyReaderFilter());
  registrationBean.addUrlPatterns("/*");
  registrationBean.setName("koalaSignFilter");
  return registrationBean;
  }
3. 獲取請求體

既然request對象流已經換成了wrapper reqest,那么流就可以重復讀取了。接下來就是獲取。

在攔截器中,直接從request中獲取流,並進行讀取:

  /**
  * 獲取請求體內容
  * @return
  * @throws IOException
  */
  private Map<String, Object> getParamsFromRequestBody(HttpServletRequest request) throws IOException {
  BufferedReader reader = request.getReader();
   
  StringBuilder builder = new StringBuilder();
  try {
  String line = null;
  while((line = reader.readLine()) != null) {
  builder.append(line);
  }
  String bodyString = builder.toString();
  return objectMapper.readValue(bodyString, Map.class);
  } catch (Exception e) {
  e.printStackTrace();
  } finally {
  try {
  reader.close();
  } catch (IOException e) {
  e.printStackTrace();
  }
  }
  return new HashMap<>();
  }

補充

在網上找了一些關於“springboot請求體中流不可重復讀取問題”的相關文章,解決了自己的問題,但是覺得整個邏輯不怎么清晰(可能是自己沒理解?) 因此,根據自己的思路重新整理了一遍。

參考文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM