一、问题简述

先说下为啥有这个需求,在基于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'

我这边演示下效果,可以发现,两次都读出来了:

spring应用中多次读取http post方法中的流(附源码)的更多相关文章

  1. Spring配置cache(concurrentHashMap,guava cache、redis实现)附源码

    在应用程序中,数据一般是存在数据库中(磁盘介质),对于某些被频繁访问的数据,如果每次都访问数据库,不仅涉及到网络io,还受到数据库查询的影响:而目前通常会将频繁使用,并且不经常改变的数据放入缓存中,从 ...

  2. 微信小程序中如何实现分页下拉加载?(附源码)

    转眼间坚持写教你微信小程序系列已经有十节系列课程了,每天的工作压力繁重,小女子也不知道自己还能坚持这样的系列教程多久.只希望每篇教程真的对大家有帮助.这节课我们要介绍的就是如何实现分页的下拉加载,我们 ...

  3. Spring中@Transactional事务回滚(含实例详细讲解,附源码)

    一.使用场景举例 在了解@Transactional怎么用之前我们必须要先知道@Transactional有什么用.下面举个栗子:比如一个部门里面有很多成员,这两者分别保存在部门表和成员表里面,在删除 ...

  4. Spring AOP实现方式四之注入式AspectJ切面【附源码】

    现在我们要讲的是第四种AOP实现之注入式AspectJ切面 通过简单的配置就可以实现AOP了. 源码结构: 1.首先我们新建一个接口,love 谈恋爱接口. package com.spring.ao ...

  5. Spring AOP实现方式三之自动扫描注入【附源码】

    注解AOP实现  这里唯一不同的就是application 里面 不需要配置每个bean都需要配置了,直接自动扫描 注册,主要知识点是怎么通过配置文件得到bean, 注意类前面的@注解. 源码结构: ...

  6. Spring AOP实现方式三【附源码】

    注解AOP实现 源码结构: 1.首先我们新建一个接口,love 谈恋爱接口. package com.spring.aop; /** * 谈恋爱接口 * * @author Administrator ...

  7. 读取xml文件转成List<T>对象的两种方法(附源码)

    读取xml文件转成List<T>对象的两种方法(附源码) 读取xml文件,是项目中经常要用到的,所以就总结一下,最近项目中用到的读取xml文件并且转成List<T>对象的方法, ...

  8. ArcGIS紧凑型切片读取与应用1-解析(附源码)

    1.前言 ArcGIS 发布的切片服务分为紧凑型切片和传统的分散型切片以及最新ArcGIS por新增的矢量切片.传统的分散型切片面临的问题是文件个数太多,部署拷贝过程十分的耗时,紧凑型切片是对分散型 ...

  9. ArcGIS紧凑型切片读取与应用3-紧凑型批量转分散型(附源码)

    1.前言 上篇介绍了webgis动态加载解析紧凑型切片的例子,现在我们使用逆向思维实现紧凑型切片转分散型切片,在实际工作中很有用处,紧凑型切片易于拷贝,但读取只有部署到Arcgis Server才行. ...

随机推荐

  1. PHP中全局变量的使用global和$GLOBALS[]

    From: http://blog.csdn.net/happyqyt/article/details/7219889 用PHP开发项目,不可避免的会使用到全局变量,比如一些网站的配置信息,全站通用, ...

  2. 机器学习——大数据与MapReduce

    MapReduce是一个分布式计算框架 优点:可在短时间内完成大量工作 缺点:算法必须经过重写,需要对系统工程有一定的理解 使用数据类型:数值型和标称型数据 MapReduce在大量节点组成的集群上运 ...

  3. xshell使用xftp传输文件 使用pure-ftpd搭建ftp服务

    xshell使用xftp传输文件 下载xftp5 https://www.baidu.com/link?url=8rtxgX3JRIbUFO1Samzv5aXhfwRG7Cf8i4vi573QexoH ...

  4. 【伪装位置神器】神行者AnyLocation 1.3.0001可用于微信,陌陌

    <ignore_js_op> 软件名称:神行者(破解)软件版本:v1.3.0001授权类别:免费测试机型:大可乐手机 下载链接: http://pan.baidu.com/s/1qWwSM ...

  5. Postman测试api@RequestBody接收参数

    api postman 结果 { , "message": "Content type 'multipart/form-data;boundary=----------- ...

  6. php下webservice使用总结

    基于thinkphp3.2的 1.修改php配置 php.ini extension=php_soap.dll soap.wsdl_cache_enabled=0 2.soap有两种模式 wsdl和 ...

  7. MTK 关闭安全模式

    1.当 Android 设备在安全模式(Safe Mode)下工作时,任何的第三方应用程序或相关文件(主要为apk应用程序文件)都不可以使用,但可以使用 Android 设备的任务管理器选项进行卸载或 ...

  8. iOS 开发,工程中混合使用 ARC 和非ARC(转)

    [前提知识] ARC:Automatic Reference Counting,自动引用计数 在开发 iOS 3 以及之前的版本的项目时我们要自己负责使用引用计数来管理内存,比如要手动 retain. ...

  9. Floyd 和 bellman 算法

    Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包. F ...

  10. NetBpm 数据库(9)

    原文:http://blog.csdn.net/adicovofer/article/details/1718592 关注NetBpm也很久了,可是一直没有静下心来研究,为了生活的琐事,太过浮躁……今 ...