背景
寫程式碼的時候遇到 HttpServletRequest 裡的資料為null, 感覺很奇怪,同樣的請求,剛才還不為null。
排查
經過debug發現,HttpServletRequest 在 106行 之前有值, 但在106行之後為null。
而這個方法只是簡單地讀了一遍 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
。
而我們對這個流讀資料其實 呼叫了 InputStream的 read()方法。
讀取流的時候會根據position來獲取當前位置,每讀取一次,該標誌就會移動一次,如果讀到最後,read()返回-1,表示已經讀取完了。
while(inputStream.read(b)) != -1) {
}
如果想要重新讀取,可以呼叫 inputstream.reset() 方法,
但是能否 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();