一, 问题简述
先说下为啥有这个需求, 在基于 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'
我这边演示下效果, 可以发现, 两次都读出来了:
来源: https://www.cnblogs.com/grey-wolf/p/9953661.html