解決HttpServletRequest的輸入流只能讀取一次的問題

背景

通常對安全性有要求的接口都會對請求參數做一些簽名驗證,而我們一般會把驗籤的邏輯統一放到過濾器或攔截器裏,這樣就不用每個接口都去重複編寫驗籤的邏輯。

在一個項目中會有很多的接口,而不同的接口可能接收不同類型的數據,例如表單數據和json數據,表單數據還好說,調用request的getParameterMap就能全部取出來。而json數據就有些麻煩了,因爲json數據放在body中,我們需要通過request的輸入流去讀取。

但問題在於request的輸入流只能讀取一次不能重複讀取,所以我們在過濾器或攔截器裏讀取了request的輸入流之後,請求走到controller層時就會報錯。而本文的目的就是介紹如何解決在這種場景下遇到HttpServletRequest的輸入流只能讀取一次的問題。

注:本文代碼基於SpringBoot框架


HttpServletRequest的輸入流只能讀取一次的原因

我們先來看看爲什麼HttpServletRequest的輸入流只能讀一次,當我們調用getInputStream()方法獲取輸入流時得到的是一個InputStream對象,而實際類型是ServletInputStream,它繼承於InputStream。

InputStream的read()方法內部有一個postion,標誌當前流被讀取到的位置,每讀取一次,該標誌就會移動一次,如果讀到最後,read()會返回-1,表示已經讀取完了。如果想要重新讀取則需要調用reset()方法,position就會移動到上次調用mark的位置,mark默認是0,所以就能從頭再讀了。調用reset()方法的前提是已經重寫了reset()方法,當然能否reset也是有條件的,它取決於markSupported()方法是否返回true。

InputStream默認不實現reset(),並且markSupported()默認也是返回false,這一點查看其源碼便知:
解決HttpServletRequest的輸入流只能讀取一次的問題

我們再來看看ServletInputStream,可以看到該類沒有重寫mark()reset()以及markSupported()方法:
解決HttpServletRequest的輸入流只能讀取一次的問題

綜上,InputStream默認不實現reset的相關方法,而ServletInputStream也沒有重寫reset的相關方法,這樣就無法重複讀取流,這就是我們從request對象中獲取的輸入流就只能讀取一次的原因。


使用HttpServletRequestWrapper + Filter解決輸入流不能重複讀取問題

既然ServletInputStream不支持重新讀寫,那麼爲什麼不把流讀出來後用容器存儲起來,後面就可以多次利用了。那麼問題就來了,要如何存儲這個流呢?

所幸JavaEE提供了一個 HttpServletRequestWrapper類,從類名也可以知道它是一個http請求包裝器,其基於裝飾者模式實現了HttpServletRequest界面,部分源碼如下:
解決HttpServletRequest的輸入流只能讀取一次的問題

從上圖中的部分源碼可以看到,該類並沒有真正去實現HttpServletRequest的方法,而只是在方法內又去調用HttpServletRequest的方法,所以我們可以通過繼承該類並實現想要重新定義的方法以達到包裝原生HttpServletRequest對象的目的。

首先我們要定義一個容器,將輸入流裏面的數據存儲到這個容器裏,這個容器可以是數組或集合。然後我們重寫getInputStream方法,每次都從這個容器裏讀數據,這樣我們的輸入流就可以讀取任意次了。

 

實現代碼

package com.eshore.ismp.filter;


import org.springframework.util.StreamUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

/**
 * Created by lihaodi on 2016/4/7.
 */
public class RequestWrapper extends HttpServletRequestWrapper {

    private  byte[] requestBody;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
    }

    @Override
    public ServletInputStream getInputStream() {
        if (requestBody == null) {
            requestBody = new byte[0];
        }
        final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {

            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }
}

  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章