溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何解決HttpServletRequest流數(shù)據(jù)不可重復(fù)讀的操作

發(fā)布時間:2021-08-24 17:28:44 來源:億速云 閱讀:299 作者:chen 欄目:開發(fā)技術(shù)

這篇文章主要講解了“如何解決HttpServletRequest流數(shù)據(jù)不可重復(fù)讀的操作”,文中的講解內(nèi)容簡單清晰,易于學(xué)習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習“如何解決HttpServletRequest流數(shù)據(jù)不可重復(fù)讀的操作”吧!

目錄
  • 前言

  • ServletRequest 數(shù)據(jù)封裝原理

    • Spring MVC 對不同類型數(shù)據(jù)的封裝

    • 讀取參數(shù)時出現(xiàn)的問題

      • 具體的問題可以細分成多種情況:

  • 最佳解決方案

    • tips:

    • 總結(jié)

      • 附錄代碼

        前言

        在某些業(yè)務(wù)中可能會需要多次讀取 HTTP 請求中的參數(shù),比如說前置的 API 簽名校驗。這個時候我們可能會在攔截器或者過濾器中實現(xiàn)這個邏輯,但是嘗試之后就會發(fā)現(xiàn),如果在攔截器中通過 getInputStream() 讀取過參數(shù)后,在 Controller 中就無法重復(fù)讀取了,會拋出以下幾種異常:

        HttpMessageNotReadableException: Required request body is missing

        IllegalStateException: getInputStream() can't be called after getReader()

        這個時候需要我們將請求的數(shù)據(jù)緩存起來。本文會從 ServletRequest 數(shù)據(jù)封裝原理開始詳細講講如何解決這個問題。如果不想看原理的,可直接閱讀 最佳解決方案。

        ServletRequest 數(shù)據(jù)封裝原理

        平時我們接受 HTTP 請求的參數(shù)時,基本是通過 SpringMVC 的包裝。

        • POST form-data 參數(shù)時,直接用實體類,或者直接在 Controller 的方法上把參數(shù)填上就可以了,手動則可以通過 request.getParameter() 來獲取。

        • POST json 時,會在實體類上添加 @RequestBody 參數(shù)或者直接調(diào)用 request.getInputStream() 獲取流數(shù)據(jù)。

        我們可以發(fā)現(xiàn)在獲取不同數(shù)據(jù)格式的數(shù)據(jù)時調(diào)用的方法是不同的,但是閱讀源碼可以發(fā)現(xiàn),其實底層他們的數(shù)據(jù)來源都是一樣的,只是 SpringMVC 幫我們做了一下處理。下面我們就來講講 ServletRequest 數(shù)據(jù)封裝的原理。

        實際上我們通過 HTTP 傳輸?shù)膮?shù)都會存在 Request 對象的 InputStream 中,這個 Request 對象也就是 ServletRequest 最終的實現(xiàn),是由 tomcat 提供的。然后針對于不同的數(shù)據(jù)格式,會在不同的時刻對 InputStream 中的數(shù)據(jù)進行封裝。

        Spring MVC 對不同類型數(shù)據(jù)的封裝

        • GET 請求的數(shù)據(jù)一般是 Query String,直接在 url 的后面,不需要特殊處理

        • 通過例如 POST、PUT 發(fā)送 multipart/form-data 格式的數(shù)據(jù)

        // 源碼中適當去除無關(guān)代碼
        // 對于這類數(shù)據(jù),SpringMVC 在 DispatchServlet 的 doDispatch() 方法中就會進行處理。具體處理流程如下:
        // org.springframework.web.servlet.DispatcherServlet.java
        protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
            HttpServletRequest processedRequest = request;
            HandlerExecutionChain mappedHandler = null;
            boolean multipartRequestParsed = false;
            WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);
            // Determine handler for the current request.
            // other code...
        }
        // 1. 調(diào)用 checkMultipart(request),當前請求的數(shù)據(jù)類型是否為 multipart/form-data
        protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
            if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
        		return this.multipartResolver.resolveMultipart(request);
            }
            return request;
        }
        //2. 如果是,調(diào)用 multipartResolver 的 resolveMultipart(request),返回一個 StandardMultipartHttpServletRequest 對象。
        // org.springframework.web.multipart.support.StandardMultipartHttpServletRequest.java
        public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
            this(request, false);
        }
        public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
            super(request);
            if (!lazyParsing) {
                parseRequest(request);
            }
        }
        // 3. 在構(gòu)造 StandardMultipartHttpServletRequest 對象時,會調(diào)用 parseRequest(request),將 InputStream 中是數(shù)據(jù)流進行進一步的封裝。
        // 不貼源碼了,主要是對 form-data 數(shù)據(jù)的封裝,包含字段和文件。
        • 通過例如 POST、PUT 發(fā)送 application/x-www-form-urlencoded 格式的數(shù)據(jù)

        // 非 form-data 的數(shù)據(jù),會存儲在 HttpServletRequest 的 InputStream 中。
        // 在第一次調(diào)用 getParameterNames() 或 getParameter() 時,
        // 會調(diào)用 parseParameters() 方法對參數(shù)進行封裝,從 InputStream 中讀取數(shù)據(jù),并封裝到 Map 中。
        //org.apache.catalina.connector.Request.java
        public String getParameter(String name) {
            if (!this.parametersParsed) {
                this.parseParameters();
            }
            return this.coyoteRequest.getParameters().getParameter(name);
        }
        • 通過例如 POST、PUT 發(fā)送 application/json 格式的數(shù)據(jù)

        // 數(shù)據(jù)會直接會存儲在 HttpServletRequest 的 InputStream 中,通過 request.getInputStream() 或 getReader() 獲取。

        讀取參數(shù)時出現(xiàn)的問題

        現(xiàn)在我們基本已經(jīng)對 SpringMVC 是如何封裝 HTTP 請求參數(shù)有了一定的認識。根據(jù)之前描述的,我們?nèi)绻跀r截器中和 Controller 中重復(fù)讀取參數(shù)時,會出現(xiàn)以下異常:

        HttpMessageNotReadableException: Required request body is missing

        IllegalStateException: getInputStream() can't be called after getReader()

        這是由于 InputStream 這個流數(shù)據(jù)的特殊性,在 Java 中讀取 InputStream 數(shù)據(jù)時,內(nèi)部是通過一個指針的移動來讀取一個一個的字節(jié)數(shù)據(jù)的,當讀完一遍后,這個指針并不會 reset,因此第二遍讀的時候就會出現(xiàn)問題了。而之前講了,HTTP 請求的參數(shù)也是封裝在 Request 對象中的 InputStream 里,所以當?shù)诙握{(diào)用 getInputStream() 時會拋出上述異常。

        具體的問題可以細分成多種情況:

        1、請求方式為 multipart/form-data,在攔截器中手動調(diào)用 request.getInputStream()

        // 上文講了在 doDispatch() 時就會進行處理,因此這里會取不到值
        log.info("input stream content: {}", new String(StreamUtils.copyToByteArray(request.getInputStream())));

        2、請求方式為 application/x-www-form-urlencoded,在攔截器中手動調(diào)用 request.getInputStream()

        // 第 1 次可以取到值
        log.info("input stream content: {}", new String(StreamUtils.copyToByteArray(request.getInputStream())));
        // 第一次執(zhí)行 getParameter() 會調(diào)用 parseParameters(),parseParameters 進一步調(diào)用 getInputStream()
        // 這里就取不到值了
        log.info("form-data param: {}", request.getParameter("a"));
        log.info("form-data param: {}", request.getParameter("b"));

        3、請求方式為 application/json,在攔截器中手動調(diào)用 request.getInputStream()

        // 第 1 次可以取到值
        log.info("input stream content: {}", new String(StreamUtils.copyToByteArray(request.getInputStream())));
        // 之后再任何地方再調(diào)用 getInputStream() 都取不到值,會拋出異常

        為了能夠多次獲取到 HTTP 請求的參數(shù),我們需要將 InputStream 流中的數(shù)據(jù)緩存起來。

        最佳解決方案

        通過查閱資料,實際上 springframework 自己就有相應(yīng)的 wrapper 來解決這個問題,在 org.springframework.web.util 包下有一個 ContentCachingRequestWrapper 的類。這個類的作用就是將 InputStream 緩存到 ByteArrayOutputStream 中,通過調(diào)用 ``getContentAsByteArray()` 實現(xiàn)流數(shù)據(jù)的可重復(fù)讀取。

        /**
         * {@link javax.servlet.http.HttpServletRequest} wrapper that caches all content read from
         * the {@linkplain #getInputStream() input stream} and {@linkplain #getReader() reader},
         * and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.
         * @see ContentCachingResponseWrapper
         */

        在使用上,只需要添加一個 Filter,將 HttpServletRequest 包裝成 ContentCachingResponseWrapper 返回給攔截器和 Controller 就可以了。

        @Slf4j
        @WebFilter(urlPatterns = "/*")
        public class CachingContentFilter implements Filter {
            private static final String FORM_CONTENT_TYPE = "multipart/form-data";
            @Override
            public void init(FilterConfig filterConfig) {
            }
            @Override
            public void doFilter(ServletRequest request, ServletResponse response,
                                 FilterChain chain) throws IOException, ServletException {
                String contentType = request.getContentType();
                if (request instanceof HttpServletRequest) {
                    HttpServletRequest requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
                    // #1
                    if (contentType != null && contentType.contains(FORM_CONTENT_TYPE)) {
                        chain.doFilter(request, response);
                    } else {
                        chain.doFilter(requestWrapper, response);
                    }
                    return;
                }
                chain.doFilter(request, response);
            }
            @Override
            public void destroy() {
            }
        }
        // 添加掃描 filter 注解
        @ServletComponentScan
        @SpringBootApplication
        public class SeedApplication {
            public static void main(String[] args) {
                SpringApplication.run(SeedApplication.class, args);
            }
        }

        在攔截器中,獲取請求參數(shù):

        // 流數(shù)據(jù)獲取,比如 json
        // #2
        String jsonBody = IOUtils.toString(wrapper.getContentAsByteArray(), "utf-8");
        // form-data 和 urlencoded 數(shù)據(jù)
        String paramA = request.getParameter("paramA");
        Map<String,String[]> params = request.getParameterMap();

        tips:

        1、這里需要根據(jù) contentType 做一下區(qū)分,遇到 multipart/form-data 數(shù)據(jù)時,不需要 wrapper,會直接通過 MultipartResolver 將參數(shù)封裝成 Map,當然這也可以靈活的在攔截器中判斷。

        2、wrapper 在具體使用中,我們可以使用 getContentAsByteArray() 來獲取數(shù)據(jù),并通過 IOUtils 轉(zhuǎn)換成 String。盡量不使用 request.getInputStream()。因為雖然經(jīng)過了包裝,但是 InputStream 仍然只能讀一次,而參數(shù)進入 Controller 的方法前 HttpMessageConverter 的參數(shù)轉(zhuǎn)換需要調(diào)用這個方法,所以把它保留就可以了。

        總結(jié)

        遇到這個問題的時候也參考了很多博客,有的使用了 ContentCachingRequestWrapper,也有的自己實現(xiàn)了一個 Wrapper。但是自己實現(xiàn) Wrapper 的方案,多半是直接在 Wrapper 的構(gòu)造函數(shù)中讀取流數(shù)據(jù)到 byte[] 數(shù)據(jù)中去,這樣在遇到 multipart/form-data 這種數(shù)據(jù)類型的時候就會出現(xiàn)問題了,因為包裝在調(diào)用 MultipartResolver 之前執(zhí)行,再次調(diào)用的時候就讀不到數(shù)據(jù)了。

        所以博主又自己研究了一下 Spring 的源碼,實現(xiàn)了這種方案,基本上可以處理多種通用的數(shù)據(jù)類型了。

        附錄代碼

        package com.example.seed.common.config;
        import lombok.extern.slf4j.Slf4j;
        import org.springframework.web.util.ContentCachingRequestWrapper;
        import javax.servlet.*;
        import javax.servlet.annotation.WebFilter;
        import javax.servlet.http.HttpServletRequest;
        import java.io.IOException;
        /**
         * @author Fururur
         * @date 2020/5/6-14:26
         */
        @Slf4j
        @WebFilter(urlPatterns = "/*")
        public class CachingContentFilter implements Filter {
            private static final String FORM_CONTENT_TYPE = "multipart/form-data";
            @Override
            public void init(FilterConfig filterConfig) {
            }
            @Override
            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
                String contentType = request.getContentType();
                if (request instanceof HttpServletRequest) {
                    HttpServletRequest requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
                    if (contentType != null && contentType.contains(FORM_CONTENT_TYPE)) {
                        chain.doFilter(request, response);
                    } else {
                        chain.doFilter(requestWrapper, response);
                    }
                    return;
                }
                chain.doFilter(request, response);
            }
            @Override
            public void destroy() {
            }
        }
        package com.example.seed;
        import org.springframework.boot.SpringApplication;
        import org.springframework.boot.autoconfigure.SpringBootApplication;
        import org.springframework.boot.web.servlet.ServletComponentScan;
        @ServletComponentScan
        @SpringBootApplication
        public class SeedApplication {
            public static void main(String[] args) {
                SpringApplication.run(SeedApplication.class, args);
            }
        }
        @RequestMapping("/query")
        public void query(HttpServletRequest request) {
            ContentCachingRequestWrapper wrapper = new ContentCachingRequestWrapper(request);
            log.info("{}", new String(wrapper.getContentAsByteArray()));
        }

        感謝各位的閱讀,以上就是“如何解決HttpServletRequest流數(shù)據(jù)不可重復(fù)讀的操作”的內(nèi)容了,經(jīng)過本文的學(xué)習后,相信大家對如何解決HttpServletRequest流數(shù)據(jù)不可重復(fù)讀的操作這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關(guān)知識點的文章,歡迎關(guān)注!

        向AI問一下細節(jié)

        免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

        AI