欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2024-03-05 13:47:30
一、问题简述 先说下为啥有这个需求,在基于spring的web应用中,一般会在controller层获取http方法body中的数据。 方式1: 比如http请求的c...

一、问题简述

先说下为啥有这个需求,在基于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://*.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://*.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) {
  }
}

至此。完整的偷梁换柱就结束了。

现在,请再回过头去,看文章开头的代码,标红的部分。

是不是豁然开朗了?

三、代码地址

直接git 下载即可。

这是个单独的工程,直接eclipse或者idea导入即可。

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

运行方法:

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

我这边讲下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方法中的流遇到的问题

总结

以上所述是小编给大家介绍的spring应用中多次读取http post方法中的流,希望对大家有所帮助