zoukankan      html  css  js  c++  java
  • 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'

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

  • 相关阅读:
    mybatis由浅入深day01_6SqlMapConfig.xml(6.2settings全局参数配置_6.3typeAliases(类型别名)_6.4typeHandlers(类型处理器)_6.5mappers(映射配置))
    mybatis由浅入深day01_5.3 Mapper动态代理方法
    mybatis由浅入深day01_5mybatis开发dao的方法(5.1SqlSession使用范围_5.2原始dao开发方法)
    mybatis由浅入深day01_ 4.11总结(parameterType_resultType_#{}和${}_selectOne和selectList_mybatis和hibernate本质区别和应用场景)
    mybatis由浅入深day01_4.9删除用户_4.10更新用户
    mybatis由浅入深day01_4.7根据用户名称模糊查询用户信息_4.8添加用户((非)自增主键返回)
    mybatis由浅入深day01_4入门程序_4.6根据用户id(主键)查询用户信息
    mybatis由浅入深day01_3mybatis框架介绍
    mybatis由浅入深day01_1课程安排_2对原生态jdbc程序中问题总结
    工作流JBPM_day02:3-预定义的活动1_4-预定义的活动2+在图片上高亮显示正在执行的上活动
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/9953661.html
Copyright © 2011-2022 走看看