spring应用中多次读取http post方法中的流遇到的问题

一、问题简述

先说下为啥有这个需求,在基于spring的web应用中,一般会在controller层获取http方法body中的数据。

方式1:

比如http请求的content-typeapplication/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方法中的流,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • 详解Spring Cloud Gateway 限流操作

    开发高并发系统时有三把利器用来保护系统:缓存.降级和限流. API网关作为所有请求的入口,请求量大,我们可以通过对并发访问的请求进行限速来保护系统的可用性. 常用的限流算法比如有令牌桶算法,漏桶算法,计数器算法等. 在Zuul中我们可以自己去实现限流的功能 (Zuul中如何限流在我的书 <Spring Cloud微服务-全栈技术与案例解析>  中有详细讲解) ,Spring Cloud Gateway的出现本身就是用来替代Zuul的. 要想替代那肯定得有强大的功能,除了性能上的优势之外,Spr

  • spring boot activiti工作流的搭建与简单使用

    前言 最近一直研究springboot,根据工作需求,工作流需要作为一个单独的微服务工程来提供给其他服务调用,现在简单的写下工作流(使用的activiti)微服务的搭建与简单使用 jdk:1.8 数据库:mysql  5.7 IDE:eclipse springboot:1.5.8 activiti:6.0.0 1.新建空白的maven微服务架构 新建maven项目的流程不在阐述,这里添加上activiti.myslq连接的依赖,只贴主要代码 pox.xml <project xmlns="

  • 最流行的java后台框架spring quartz定时任务

    配置quartz 在spring中需要三个jar包: quartz-1.8.5.jar.commons-collections-3.2.1.jar.commons-logging-1.1.jar 首先要配置我们的spring.xml xmlns 多加下面的内容. xmlns:task="http://www.springframework.org/schema/task" 然后xsi:schemaLocation多加下面的内容. http://www.springframework.o

  • 使用Spring来创建一个简单的工作流引擎

    文章来源:matrix 作者:Steve Dodge 摘要 spring是支持控制反转编程机制的一个相对新的框架.本文把spring作为简单工作流引擎,将它用在了更加通用的地方.在对工作流简单介绍之后,将要介绍在基本工作流场景中基于Spring的工作流API的使用. 许多J2EE应用程序要求在一个和主机分离的上下文中执行处理过程.在许多情况下,这些后台的进程执行多个任务,一些任务依赖于以前任务的状态.由于这些处理任务之间存在相互依赖的关系,使用一套基于过程的方法调用常常不能满足要求.开发人员能够

  • SpringBoot+LayIM+t-io 实现好友申请通知流程

    前言 在上一篇 Spring boot + LayIM + t-io 文件上传. 监听用户状态的实现 中,已经介绍了两个小细节:用户的在离线状态和群人数的状态变化.今天的主要内容就是用户加好友的实现. 简介 加好友,大家用过QQ都知道,无非是发起好友申请,对方收到消息通知,然后处理.不过,本篇只讲前半部分,消息通知的处理留到下一篇去讲.因为内容有点多,怕是一时半会消化不了.在介绍主体流程之前,先给大家介绍一下准备工作. 准备工作 首先,为了让数据更贴近实战,所以我用了比较"真实"的用户

  • 浅谈SpringMVC的执行流程

    #简易版 1.客户发送请求经过 DisPatcherServlet 核心过滤器 2.DisPatcherServlet 核心控制器在去找一个或多个HandlerMappering 找到需要处理的Controller 3.DisPatcherServlet 通过HandlerAdapter将请求转发给 Controller 4.Controller 调用业务处理后返回结果给 ModelAndView 5.DisPatcherServlet 找到一个或者多个 ViewResolver 视图解析器 找

  • spring应用中多次读取http post方法中的流遇到的问题

    一.问题简述 先说下为啥有这个需求,在基于spring的web应用中,一般会在controller层获取http方法body中的数据. 方式1: 比如http请求的content-type为application/json的情况下,直接用@RequestBody接收. 方式2: 也有像目前我们在做的这个项目,比较原始,是直接手动读取流.(不要问我为啥这么原始,第一版也不是我写的.) @RequestMapping("/XXX.do") public void XXX(HttpServl

  • thinkPHP中配置的读取与C方法详解

    本文实例讲述了thinkPHP中配置的读取与C方法.分享给大家供大家参考,具体如下: 1.项目公共配置 Conf/config.php 内容如下 <?php /** *项目公共配置 *@package *@author **/ return array( 'LOAD_EXT_CONFIG' => 'db,info,email,safe,upfile,cache,route,app,alipay,sms,platform,store,pay', 'APP_AUTOLOAD_PATH' =>

  • 基于DATAFRAME中元素的读取与修改方法

    DATAFRAME中使用iat[1,0]和iloc[0,1]对元素进行修改. a = [("hahaha",1),("lalala",2),("cacaca",6)] b = padas.DataFrame(a) b.iat[1,0] = 1.0 将位置横竖坐标为1,0的元素改为值为1.0. 以上这篇基于DATAFRAME中元素的读取与修改方法就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持我们.

  • php中对xml读取的相关函数的介绍一

    对象 XML解析函数 描述  元素 xml_set_element_handler() 元素的开始和结束  字符数据 xml_set_character_data_handler() 字符数据的开始  外部实体 xml_set_external_entity_ref_handler() 外部实体出现  未解析外部实体 xml_set_unparsed_entity_decl_handler() 未解析的外部实体出现  处理指令 xml_set_processing_instruction_han

  • Spring中配置和读取多个Properties文件的方式方法

    一个系统中通常会存在如下一些以Properties形式存在的配置文件 1.数据库配置文件demo-db.properties: database.url=jdbc:mysql://localhost/smaple database.driver=com.mysql.jdbc.Driver database.user=root database.password=123 2.消息服务配置文件demo-mq.properties: #congfig of ActiveMQ mq.java.namin

  • 详解Spring中Bean的加载的方法

    之前写过bean的解析,这篇来讲讲bean的加载,加载要比bean的解析复杂些,从之前的例子开始. Spring中加载一个bean的方式: TestBean bean = factory.getBean("testBean"); 来看看getBean(String name)方法源码, @Override public Object getBean(String name) throws BeansException { return doGetBean(name, null, nul

  • Spring加载配置和读取多个Properties文件的讲解

    一个系统中通常会存在如下一些以Properties形式存在的配置文件 1.数据库配置文件demo-db.properties: database.url=jdbc:mysql://localhost/smaple database.driver=com.mysql.jdbc.Driver database.user=root database.password=123 2.消息服务配置文件demo-mq.properties: #congfig of ActiveMQ mq.java.namin

  • Spring TransactionalEventListener事务未提交读取不到数据的解决

    目录 一.背景 二.问题分析 2.1.mysql隔离级别 2.2.问题原因分析 三.解决问题方案 3.1.方式一 3.2.方式二 四.使用案例 一.背景 业务处理过程,发现了以下问题,代码一是原代码能正常执行,代码二是经过迭代一次非正常执行代码 代码一:以下代码开启线程后,代码正常执行 ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS, new ArrayBlockingQ

  • Spring/SpringBoot @RequestParam注解无法读取application/json格式数据问题解决

    目录 前言 一.RequestMappingHandlerAdapter 二.HandlerMethodArgumentResolver 三.RequestParamMethodArgumentResolver 四.MyHandlerMethodArgumentResolver 四.ConfigArgumentResolvers 五.MyHttpServletRequestWrapper 六.HttpServletRequestReplacedFilter 七.总结 总结 前言 Emmmm…最近

  • js读取json文件片段中的数据实例

    在html中利用js读取动态网站从服务器端返回的数据进行显示 1.js.html 页面 需要引入 执行jquery的js文件 <HTML> <HEAD> <META name=Generator content=EditPlus> <META name=Author content=""> <META name=Keywords content=""> <META name=Description c

随机推荐