post方法 request.getInputStream()為空解惑

前言

在SpringMVC web應(yīng)用中,對于一個rest接口,獲取請求參數(shù)我們一般使用@requestParam、@requestBody等注解 。對于表單類型的請求參數(shù),有一下幾種獲取方式

  1. @requestParam注解方式
  2. request.getParameter(String name)
  3. request.getInputStream()

前兩種方式其實是一種方式,@requestParam底層就是利用request.getParameter的原理。這兩種方式有一個弊端就是只能一個個獲取,而且必須知道對方傳過來的參數(shù)的key值,如果想要一次性獲取,可以使用request.getInputStream方法獲取一個inputStream對象,然后讀取流里面的數(shù)據(jù)。

//獲取到的數(shù)據(jù)格式key=value以‘&’分隔的形式
age=20&name=faderw

問題

但在實際過程中,我們會發(fā)現(xiàn)通過request.getInputStream()方式獲取的數(shù)據(jù)為空。

根據(jù)Servlet規(guī)范,如果同時滿足下列條件,則請求體(Entity)中的表單數(shù)據(jù),將被填充到request的parameter集合中(request.getParameter系列方法可以讀取相關(guān)數(shù)據(jù))

  1. 這是一個HTTP/HTTPS請求
  2. 請求方法是POST(querystring無論是否POST都將被設(shè)置到parameter中)
  3. 請求的類型(Content-Type頭)是application/x-www-form-urlencoded
  4. Servlet調(diào)用了getParameter系列方法

這里的表單數(shù)據(jù)已經(jīng)被填充到parameterMap中,不能再通過getInputStream獲取。

如何解決這個問題呢。

實現(xiàn)

在javax.servlet.http包下面有一個裝飾器類HttpServletRequestWrapper,利用這個裝飾器類,我們可以重新包裝一個HttpServletRequest對象。

public class HttpServletRequestWrapper extends ServletRequestWrapper implements
        HttpServletRequest {

定義一個裝飾器繼承HttpServletRequestWrapper,streamBody字節(jié)變量用來保存讀取的數(shù)據(jù),以便于多次讀取。

public class InputStreamHttpServletRequestWrapper extends HttpServletRequestWrapper{


    private final byte[] streamBody;
    private static final int BUFFER_SIZE = 4096;

   
    public InputStreamHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        byte[] bytes = inputStream2Byte(request.getInputStream());
        if (bytes.length == 0 && RequestMethod.POST.name().equals(request.getMethod())) {
            //從ParameterMap獲取參數(shù),并保存以便多次獲取
            bytes = request.getParameterMap().entrySet().stream()
                    .map(entry -> {
                        String result;
                        String[] value = entry.getValue();
                        if (value != null && value.length > 1) {
                            result = Arrays.stream(value).map(s -> entry.getKey() + "=" + s)
                                    .collect(Collectors.joining("&"));
                        } else {
                            result = entry.getKey() + "=" + value[0];
                        }

                        return result;
                    }).collect(Collectors.joining("&")).getBytes();
        }

        streamBody = bytes;
    }

    private byte[] inputStream2Byte(InputStream inputStream) throws IOException {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] bytes = new byte[BUFFER_SIZE];
        int length;
        while ((length = inputStream.read(bytes, 0, BUFFER_SIZE)) != -1) {
            outputStream.write(bytes, 0, length);
        }

        return outputStream.toByteArray();
    }


    @Override
    public ServletInputStream getInputStream() throws IOException {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(streamBody);

        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener listener) {

            }

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

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }
}

聲明一個帶有HttpServletRequest入?yún)⒌臉?gòu)造器,從該參數(shù)對象的流中解析數(shù)據(jù),如果沒有則繼續(xù)從parameterMap中獲取,然后以key=value&key=value形式拼接。用streamBody接收。然后我們重寫getInputStream方法,以后每次調(diào)用getInputStream方法,其實是重新利用streamBody重新new一個流,所以可以多次讀取。

有了裝飾器后,我們就要裝飾目標(biāo)對象。我們都知道SpringMVC的一次請求會被一個個過濾器層層調(diào)用,也就是我們常說的責(zé)任鏈模式。利用Filter我們就可以在某個特定的位置裝飾HttpServletRequest對象。

public class InputStreamWrapperFilter extends OncePerRequestFilter{

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        ServletRequest servletRequest = new InputStreamHttpServletRequestWrapper(httpServletRequest);

        filterChain.doFilter(servletRequest, httpServletResponse);
    }
}

OncePerRequestFilter這個過濾器能夠保證一次請求只經(jīng)過一次過濾器,所以我們直接繼承該類就行了。

@Bean
@Order(1)
public FilterRegistrationBean inputStreamWrapperFilterRegistration() {
    FilterRegistrationBean registrationBean = new FilterRegistrationBean();
    registrationBean.setFilter(new InputStreamWrapperFilter());
    registrationBean.setName("inputStreamWrapperFilter");
    registrationBean.addUrlPatterns("/*");

    return registrationBean;
}

然后注冊該過濾器,設(shè)置優(yōu)先級為1。Spring Boot 會按照order值的大小,從小到大的順序來依次過濾。

測試

我們寫一個簡單的rest接口測試下

@PostMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Object inputStreamTest(HttpServletRequest request) throws Exception {
    String bs = IOUtils.toString(request.getInputStream(), "UTF-8");
    Map<String, String> map = Maps.newHashMapWithExpectedSize(1);
    map.put("data", bs);

    return map;
}

curl命令

curl -X POST \
  http://127.0.0.1:9003/home \
  -H 'Cache-Control: no-cache' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -H 'Postman-Token: bb6e680c-5142-4d27-b930-6efb118a505a' \
  -d 'age=20&name=wangyuxin'

結(jié)果

{
    "data": "age=20&name=wangyuxin"
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評論 19 139
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,939評論 1 92
  • 18歲的河妖離開她熟悉的小島來到陌生人的城市,開始她向往已久的大學(xué)生活,成日與舞結(jié)伴的她,生性愛玩,舞團(tuán)里...
    披頭士過馬路閱讀 355評論 0 1
  • 《柔》 云溪上密風(fēng)舞動; 松窗下疏雨柔靜。
    自命飛皇Yoes閱讀 550評論 0 1
  • iOS native 加載 H5 圖片(沙盒加載html圖片) socket的半包,粘包與分包的問題 Scoket...
    AgoniNemo閱讀 172評論 0 0

友情鏈接更多精彩內(nèi)容