1. 程式人生 > >spring應用中多次讀取http post方法中的流(附原始碼)

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'

 我這邊演示下效果,可以發現,兩次都讀出來了: