spring應(yīng)用中多次讀取http post方法中的流遇到的問題
一、問題簡述
先說下為啥有這個需求,在基于spring的web應(yīng)用中,一般會在controller層獲取http方法body中的數(shù)據(jù)。
方式1:
比如http請求的content-type為application/json的情況下,直接用@RequestBody接收。
方式2:
也有像目前我們在做的這個項目,比較原始,是直接手動讀取流。(不要問我為啥這么原始,第一版也不是我寫的。)
@RequestMapping("/XXX.do")
public void XXX(HttpServletRequest request, HttpServletResponse response) throws IOException {
JSONObject jsonObject = WebUtils.getParameters(request);
//業(yè)務(wù)處理
ResponseUtil.setResponse(response, MessageFactory.createSuccessMsg());
}
WebUtils.getParameters如下:
public static JSONObject getParameters(HttpServletRequest request) throws IOException {
InputStream is = null;
is = new BufferedInputStream(request.getInputStream(), BUFFER_SIZE);
int contentLength = Integer.valueOf(request.getHeader("Content-Length"));
byte[] bytes = new byte[contentLength];
int readCount = 0;
while (readCount < contentLength) {
readCount += is.read(bytes, readCount, contentLength - readCount);
}
String requestJson = new String(bytes, AppConstants.UTF8);
if (StringUtils.isBlank(requestJson)) {
return new JSONObject();
}
JSONObject jsonObj = JsonUtils.toJSONObject(requestJson);
return jsonObj;
}
當然,不管怎么說,都是對流進行讀取。
問題是,假如我想在controller前面加一層aop,aop里面對進入controller層的方法進行日志記錄,記錄方法參數(shù),應(yīng)該怎么辦呢。
如果是采用了方式1的話,簡單。spring已經(jīng)幫我們把參數(shù)從流里取出來,給我們提供好了,我們拿著打印一下日志即可。
如果是比較悲劇地采用了我們這種方式,參數(shù)里只有個httpServletRequest,那就只有自己去讀取流了,然而,在aop中我們把流讀了的話,
在controller層就讀不到了。
畢竟,流只能讀一次啊。
二、怎么一個流讀多次呢
說一千道一萬,流來自哪里,來自
javax.servlet.ServletRequest#getInputStream
所以,我們的思路,是不是可以這樣,定義一個filter,在filter中將request替換為我們自定義的request。
下面標紅的為自定義的request。
/**
*
*/
package com.ckl.filter;
import com.ckl.utils.BaseWebUtils;
import com.ckl.utils.MultiReadHttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* Web流多次讀寫過濾器
*
* 攔截所有請求,主要是針對第三方提交過來的請求.
* 為什么要做成可多次讀寫的流,因為可以在aop層打印日志。
* 但是不影響controller層繼續(xù)讀取該流
*
* 該filter的原理:https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once/17129256#17129256
* @author ckl
*/
@Order(1)
@WebFilter(filterName = "cacheRequestFilter", urlPatterns = "*.do")
public class CacheRequestFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(CacheRequestFilter.class);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// TODO Auto-generated method stub
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
logger.info("request uri:{}",httpServletRequest.getRequestURI());
if (BaseWebUtils.isFormPost(httpServletRequest)){
httpServletRequest = new MultiReadHttpServletRequest(httpServletRequest);
String parameters = BaseWebUtils.getParameters(httpServletRequest);
logger.info("CacheRequestFilter receive post req. body is {}", parameters);
}else if (isPost(httpServletRequest)){
//文件上傳請求,沒必要緩存請求
if (request.getContentType().contains(MediaType.MULTIPART_FORM_DATA_VALUE)){
}else {
httpServletRequest = new MultiReadHttpServletRequest(httpServletRequest);
String parameters = BaseWebUtils.getParameters(httpServletRequest);
logger.info("CacheRequestFilter receive post req. body is {}", parameters);
}
}
chain.doFilter(httpServletRequest, response);
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
public static boolean isPost(HttpServletRequest request) {
return HttpMethod.POST.matches(request.getMethod());
}
}
MultiReadHttpServletRequest.java:
import org.apache.commons.io.IOUtils;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* desc:
* https://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once/17129256#17129256
* @author : ckl
* creat_date: 2018/8/2 0002
* creat_time: 13:46
**/
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private ByteArrayOutputStream cachedBytes;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
cachedBytes = new ByteArrayOutputStream();
ServletInputStream inputStream = null;
try {
inputStream = super.getInputStream();
IOUtils.copy(inputStream, cachedBytes);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CachedServletInputStream(cachedBytes);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
在自定義的request中,構(gòu)造函數(shù)中,先把原始流中的數(shù)據(jù)讀出來,放到ByteArrayOutputStream cachedBytes中。
并且需要重新定義getInputStream方法。
以后每次程序中調(diào)用getInputStream方法時,都會從我們的偷梁換柱的request中的cachedBytes字段,new一個InputStream出來。
看上圖紅色部分:
getInputStream我們返回了自定義的CachedServletInputStream類。
那么,接下來是CachedServletInputStream:
package com.ceiec.webservice.utils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* An inputstream which reads the cached request body
*/
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream(ByteArrayOutputStream cachedBytes) {
// create a new input stream from the cached request body
byte[] bytes = cachedBytes.toByteArray();
input = new ByteArrayInputStream(bytes);
}
@Override
public int read() throws IOException {
return input.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
}
至此。完整的偷梁換柱就結(jié)束了。
現(xiàn)在,請再回過頭去,看文章開頭的代碼,標紅的部分。
是不是豁然開朗了?
三、代碼地址
https://github.com/cctvckl/work_util/tree/master/spring-mvc-multiread-post
直接git 下載即可。
這是個單獨的工程,直接eclipse或者idea導(dǎo)入即可。

運行方法:

我這邊講下idea:
直接運行jetty:run這個goal即可。
然后訪問testPost.do即可(下面把curl貼出來,可以自己在接口測試工具里拼裝):
curl -i -X POST \
-H "Content-Type:application/json" \
-d \
'{"id":"32"}
' \
'http://localhost:8080/springmvc-multiread-post/testPost.do'
我這邊演示下效果,可以發(fā)現(xiàn),兩次都讀出來了:

總結(jié)
以上所述是小編給大家介紹的spring應(yīng)用中多次讀取http post方法中的流,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
相關(guān)文章
SpringBoot 整合Redisson重寫cacheName支持多參數(shù)的案例代碼
這篇文章主要介紹了SpringBoot 整合Redisson重寫cacheName支持多參數(shù),本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-01-01
Java面試題-實現(xiàn)復(fù)雜鏈表的復(fù)制代碼分享
這篇文章主要介紹了Java面試題-實現(xiàn)復(fù)雜鏈表的復(fù)制代碼分享,小編覺得還是挺不錯的,具有參考價值,需要的朋友可以了解下。2017-10-10
教你安裝eclipse2021并配置內(nèi)網(wǎng)maven中心倉庫的圖文詳解
本文能通過圖文并茂的形式給大家介紹安裝eclipse2021并配置內(nèi)網(wǎng)maven中心倉庫的相關(guān)知識,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2021-09-09
在SpringBoot3中spring.factories配置不起作用的原因和解決方法
本文給大家介紹了在SpringBoot3中spring.factories配置的自動裝配不生效的原因和解決方法,文中通過代碼和圖文給出了詳細的解決方法,具有一定的參考價值,需要的朋友可以參考下2024-02-02
SpringBoot的@RestControllerAdvice作用詳解
這篇文章主要介紹了SpringBoot的@RestControllerAdvice作用詳解,@RestContrllerAdvice是一種組合注解,由@ControllerAdvice,@ResponseBody組成,本質(zhì)上就是@Component,需要的朋友可以參考下2024-01-01

