spring應用中屢次讀取http post方法中的流(附源碼)

1、問題簡述

先說下爲啥有這個需求,在基於spring的web應用中,通常會在controller層獲取http方法body中的數據。java

方式1:git

好比http請求的content-type爲application/json的狀況下,直接用@RequestBody接收。github

方式2:web

也有像目前咱們在作的這個項目,比較原始,是直接手動讀取流。(不要問我爲啥這麼原始,初版也不是我寫的。)spring

@RequestMapping("/XXX.do") public void XXX(HttpServletRequest request, HttpServletResponse response) throws IOException { JSONObject jsonObject = WebUtils.getParameters(request);      //業務處理
 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; }

 

固然,無論怎麼說,都是對流進行讀取。apache

問題是,假如我想在controller前面加一層aop,aop裏面對進入controller層的方法進行日誌記錄,記錄方法參數,應該怎麼辦呢。json

 

若是是採用了方式1的話,簡單。spring已經幫咱們把參數從流裏取出來,給咱們提供好了,咱們拿着打印一下日誌便可。spring-mvc

若是是比較悲劇地採用了咱們這種方式,參數裏只有個httpServletRequest,那就只有本身去讀取流了,然而,在aop中咱們把流讀了的話,緩存

在controller層就讀不到了。mvc

畢竟,流只能讀一次啊。

 

2、怎麼一個流讀屢次呢

說一千道一萬,流來自哪裏,來自

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層繼續讀取該流 * * 該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中,構造函數中,先把原始流中的數據讀出來,放到ByteArrayOutputStream cachedBytes中。

而且須要從新定義getInputStream方法。

之後每次程序中調用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) { } }

 

至此。完整的偷樑換柱就結束了。

 

如今,請再回過頭去,看文章開頭的代碼,標紅的部分。

是否是豁然開朗了?

 

3、代碼地址

https://github.com/cctvckl/work_util/tree/master/spring-mvc-multiread-post

直接git 下載便可。

 

這是個單獨的工程,直接eclipse或者idea導入便可。

 

 

運行方法:

我這邊講下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'

 

 

 我這邊演示下效果,能夠發現,兩次都讀出來了:

相關文章
相關標籤/搜索