最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

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

 更新時(shí)間:2021年08月23日 11:57:22   作者:Fururur  
這篇文章主要介紹了解決HttpServletRequest 流數(shù)據(jù)不可重復(fù)讀的操作,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

前言

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

HttpMessageNotReadableException: Required request body is missing

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

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

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

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

  • POST form-data 參數(shù)時(shí),直接用實(shí)體類,或者直接在 Controller 的方法上把參數(shù)填上就可以了,手動(dòng)則可以通過 request.getParameter() 來獲取。
  • POST json 時(shí),會(huì)在實(shí)體類上添加 @RequestBody 參數(shù)或者直接調(diào)用 request.getInputStream() 獲取流數(shù)據(jù)。

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

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

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

  • GET 請求的數(shù)據(jù)一般是 Query String,直接在 url 的后面,不需要特殊處理
  • 通過例如 POST、PUT 發(fā)送 multipart/form-data 格式的數(shù)據(jù)
// 源碼中適當(dāng)去除無關(guān)代碼
// 對于這類數(shù)據(jù),SpringMVC 在 DispatchServlet 的 doDispatch() 方法中就會(huì)進(jìn)行處理。具體處理流程如下:
// 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),當(dāng)前請求的數(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),返回一個(gè) 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 對象時(shí),會(huì)調(diào)用 parseRequest(request),將 InputStream 中是數(shù)據(jù)流進(jìn)行進(jìn)一步的封裝。
// 不貼源碼了,主要是對 form-data 數(shù)據(jù)的封裝,包含字段和文件。
  • 通過例如 POST、PUT 發(fā)送 application/x-www-form-urlencoded 格式的數(shù)據(jù)
// 非 form-data 的數(shù)據(jù),會(huì)存儲在 HttpServletRequest 的 InputStream 中。
// 在第一次調(diào)用 getParameterNames() 或 getParameter() 時(shí),
// 會(huì)調(diào)用 parseParameters() 方法對參數(shù)進(jìn)行封裝,從 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ù)會(huì)直接會(huì)存儲在 HttpServletRequest 的 InputStream 中,通過 request.getInputStream() 或 getReader() 獲取。

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

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

HttpMessageNotReadableException: Required request body is missing

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

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

具體的問題可以細(xì)分成多種情況:

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

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

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

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

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

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

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

最佳解決方案

通過查閱資料,實(shí)際上 springframework 自己就有相應(yīng)的 wrapper 來解決這個(gè)問題,在 org.springframework.web.util 包下有一個(gè) ContentCachingRequestWrapper 的類。這個(gè)類的作用就是將 InputStream 緩存到 ByteArrayOutputStream 中,通過調(diào)用 ``getContentAsByteArray()` 實(shí)現(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
 */

在使用上,只需要添加一個(gè) 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ù)時(shí),不需要 wrapper,會(huì)直接通過 MultipartResolver 將參數(shù)封裝成 Map,當(dāng)然這也可以靈活的在攔截器中判斷。

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

總結(jié)

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

所以博主又自己研究了一下 Spring 的源碼,實(shí)現(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()));
}

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Spring Web MVC和Hibernate的集成配置詳解

    Spring Web MVC和Hibernate的集成配置詳解

    這篇文章主要介紹了Spring Web MVC和Hibernate的集成配置詳解,具有一定借鑒價(jià)值,需要的朋友可以參考下
    2017-12-12
  • Java8加java10等于Java18的版本查看及特性詳解

    Java8加java10等于Java18的版本查看及特性詳解

    這篇文章主要為大家介紹了Java?8加java10等于Java18的各個(gè)版本要點(diǎn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-06-06
  • JAVA中while循環(huán)的使用與注意事項(xiàng)

    JAVA中while循環(huán)的使用與注意事項(xiàng)

    這篇文章主要介紹了while循環(huán)在編程中的應(yīng)用,包括其基本結(jié)構(gòu)、語句示例、適用場景以及注意事項(xiàng),文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2025-01-01
  • Mybatis Plus 自定義方法實(shí)現(xiàn)分頁功能的示例代碼

    Mybatis Plus 自定義方法實(shí)現(xiàn)分頁功能的示例代碼

    這篇文章主要介紹了Mybatis Plus 自定義方法實(shí)現(xiàn)分頁功能的示例代碼,代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-08-08
  • java開發(fā)中如何使用JVisualVM進(jìn)行性能分析

    java開發(fā)中如何使用JVisualVM進(jìn)行性能分析

    JVisualVM是由Sun提供的性能分析工具,如此強(qiáng)大的后盾怎能不強(qiáng)大?在Jdk6.0以后的版本中是自帶的,配置好環(huán)境變量然后在運(yùn)行中輸入“JVisualVm”或直接到Jdk的安裝目錄的Bin目錄下找到運(yùn)行程序即可運(yùn)行。如果是用Jdk1.5或以前版本的朋友就得要單獨(dú)安裝了
    2015-12-12
  • EasyExcel自定義導(dǎo)出列和順序?qū)嵗a

    EasyExcel自定義導(dǎo)出列和順序?qū)嵗a

    這篇文章主要給大家介紹了關(guān)于EasyExcel自定義導(dǎo)出列和順序的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-07-07
  • SpringBoot Maven打包如何根據(jù)環(huán)境排除文件

    SpringBoot Maven打包如何根據(jù)環(huán)境排除文件

    文章介紹了在SpringBoot項(xiàng)目中,根據(jù)不同的環(huán)境(開發(fā)、測試、生產(chǎn))進(jìn)行JSP文件打包處理的方法,通過配置`pom.xml`文件中的``標(biāo)簽,可以實(shí)現(xiàn)開發(fā)環(huán)境保留`index.jsp`文件,測試環(huán)境和生產(chǎn)環(huán)境排除該文件
    2024-12-12
  • Spring配置文件的拆分和整合過程分析

    Spring配置文件的拆分和整合過程分析

    在實(shí)際應(yīng)用里,隨著應(yīng)用規(guī)模的增加,系統(tǒng)中 Bean 數(shù)量也大量增加,導(dǎo)致配置文件非常龐大。為了避免這種情況的產(chǎn)生,提高配置文件的可讀性與可維護(hù)性,可以將Spring 配置文件分解成多個(gè)配置文件,感興趣的朋友跟隨小編一起看看吧
    2022-10-10
  • @KafkaListener 如何使用

    @KafkaListener 如何使用

    這篇文章主要介紹了@KafkaListener 如何使用,本文通過圖文實(shí)例代碼相結(jié)合給大家詳細(xì)講解,文末給大家介紹了kafka的消費(fèi)者分區(qū)分配策略,需要的朋友可以參考下
    2023-02-02
  • Java在Linux下 不能處理圖形的解決辦法 分享

    Java在Linux下 不能處理圖形的解決辦法 分享

    Java在Linux下 不能處理圖形的解決辦法 分享,需要的朋友可以參考一下
    2013-06-06

最新評論

乌鲁木齐县| 景宁| 汉阴县| 博兴县| 武胜县| 苏尼特左旗| 永平县| 乐至县| 南昌县| 乌兰察布市| 青阳县| 阿克| 宁河县| 巫山县| 铁岭市| 华亭县| 邹平县| 台东市| 汝阳县| 乌鲁木齐县| 通化市| 和平区| 深州市| 苍南县| 淮阳县| 阳东县| 礼泉县| 泾源县| 阜南县| 六安市| 营山县| 邵武市| 高唐县| 柘荣县| 南开区| 德清县| 科尔| 乌恰县| 怀集县| 万安县| 驻马店市|