spring应用中多次读取http post方法中的流(附源码)
一、问题简述
先说下为啥有这个需求,在基于spring的web应用中,一般会在controller层获取http方法body中的数据。
方式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); //业务处理
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层的方法进行日志记录,记录方法参数,应该怎么办呢。
如果是采用了方式1的话,简单。spring已经帮我们把参数从流里取出来,给我们提供好了,我们拿着打印一下日志即可。
如果是比较悲剧地采用了我们这种方式,参数里只有个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层继续读取该流 * * 该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) { } }
至此。完整的偷梁换柱就结束了。
现在,请再回过头去,看文章开头的代码,标红的部分。
是不是豁然开朗了?
三、代码地址
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'
我这边演示下效果,可以发现,两次都读出来了: