首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Spring MVC 請求參數獲取失效問題:InputStream 重複讀取解決方案

     編輯:Java 綜合教學

問題根因

HttpServletRequest.getInputStream() 只能讀取一次。實際項目中常見衝突場景:

  1. 多個攔截器/Filter 需讀取請求體(如簽名驗證、日誌記錄、風控檢查)
  2. 攔截器讀取後,Controller 層 getParameter()、@RequestParam 失效
  3. 自定義編碼(如 Unicode 轉義)需讀取原始流解碼

解決方案:Request 包裝器 + 早期 Filter 替換

核心思路

  • 緩存請求體:首次讀取時將 InputStream 內容完整讀入 byte[] 緩存
  • 多次復用:後續獲取 InputStream 時返回 ByteArrayInputStream 包裝
  • 參數重解析:重寫 getParameter* 方法,從緩存數據解析參數
  • 自定義解碼:集成業務編碼邏輯(如 Unicode 解碼)

1. 自定義 Request 包裝器

package com.example.web.filter;

import java.io.*;
import java.net.URLDecoder;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] cachedBody;
    private final Map parameterMap;

    public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 1. 緩存請求體
        this.cachedBody = readBytes(request.getInputStream());
        
        // 2. 解析參數(POST 表單 / GET 查詢字符串)
        if ("POST".equalsIgnoreCase(request.getMethod())) {
            this.parameterMap = parseParameters(new String(cachedBody, getCharacterEncoding()));
        } else {
            this.parameterMap = parseParameters(request.getQueryString());
        }
    }

    /** 讀取流全部內容到位元組陣列 */
    private byte[] readBytes(InputStream in) throws IOException {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream();
             BufferedInputStream bin = new BufferedInputStream(in)) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = bin.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        }
    }

    /** 解析 application/x-www-form-urlencoded 參數 */
    private Map parseParameters(String queryString) {
        Map map = new LinkedHashMap<>();
        if (queryString == null || queryString.isEmpty()) return map;
        
        for (String pair : queryString.split("&")) {
            int idx = pair.indexOf('=');
            if (idx < 0) continue;
            String key = pair.substring(0, idx);
            String value = idx < pair.length() - 1 ? pair.substring(idx + 1) : "";
            value = decodeValue(value); // 自定義解碼
            
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
        }
        // List[] → String[][]
        Map result = new LinkedHashMap<>();
        map.forEach((k, v) -> result.put(k, v.toArray(new String[0])));
        return result;
    }

    /** 自定義解碼:支援 Unicode \uXXXX + 標準 URL 解碼 */
    private String decodeValue(String value) {
        if (value.contains("%u")) {
            return decodeUnicode(value); // 實現 Unicode 解碼
        }
        try {
            return URLDecoder.decode(value, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            return value;
        }
    }

    private String decodeUnicode(String str) {
        // 實現 \uXXXX → 字符 轉換
        StringBuilder sb = new StringBuilder();
        int i = 0;
        while (i < str.length()) {
            if (str.startsWith("%u", i) && i + 5 < str.length()) {
                String hex = str.substring(i + 2, i + 6);
                sb.append((char) Integer.parseInt(hex, 16));
                i += 6;
            } else {
                sb.append(str.charAt(i++));
            }
        }
        return sb.toString();
    }

    // === 重寫參數獲取方法 ===
    @Override public Map getParameterMap() { return parameterMap; }
    @Override public String getParameter(String name) { 
        String[] vals = parameterMap.get(name); 
        return vals != null && vals.length > 0 ? vals[0] : null; 
    }
    @Override public String[] getParameterValues(String name) { return parameterMap.get(name); }
    @Override public Enumeration getParameterNames() { return Collections.enumeration(parameterMap.keySet()); }

    // === 重寫流獲取方法 ===
    @Override public ServletInputStream getInputStream() {
        return new ServletInputStream() {
            private final ByteArrayInputStream bais = new ByteArrayInputStream(cachedBody);
            @Override public int read() { return bais.read(); }
            @Override public boolean isFinished() { return bais.available() == 0; }
            @Override public boolean isReady() { return true; }
            @Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException(); }
        };
    }
    @Override public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream(), getCharacterEncoding()));
    }
}

2. 早期 Filter 替換 Request

package com.example.web.filter;

import java.io.IOException;
import javax.servlet.*;

public class RequestWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        
        if (request instanceof HttpServletRequest) {
            HttpServletRequest httpReq = (HttpServletRequest) request;
            // 僅包裝 POST 請求(GET 參數在 QueryString,無流衝突)
            if ("POST".equalsIgnoreCase(httpReq.getMethod())) {
                try {
                    request = new CachedBodyHttpServletRequestWrapper(httpReq);
                } catch (IOException e) {
                    // 記錄錯誤,繼續原請求
                }
            }
        }
        chain.doFilter(request, response);
    }

    @Override public void init(FilterConfig config) {}
    @Override public void destroy() {}
}

3. web.xml 配置(最優先執行)


    
    
    
    
        requestWrapperFilter
        com.example.web.filter.RequestWrapperFilter
    
    
        requestWrapperFilter
        /*
        REQUEST
        FORWARD
        INCLUDE
    
    
    
    
        signatureFilter
        com.example.web.filter.SignatureFilter
    
    

Spring Boot 2.x/3.x 配置方式

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean requestWrapperFilter() {
        FilterRegistrationBean bean = new FilterRegistrationBean<>();
        bean.setFilter(new RequestWrapperFilter());
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 最優先
        bean.addUrlPatterns("/*");
        return bean;
    }
}

進階優化:大請求體處理

防止大文件上傳撐爆記憶體:

public class CachedBodyHttpServletRequestWrapper extends HttpServletRequestWrapper {
    private static final int MEMORY_THRESHOLD = 1024 * 1024; // 1MB
    private static final int MAX_CACHE_SIZE = 10 * 1024 * 1024; // 10MB

    private final byte[] cachedBody;
    private final File tempFile; // 大請求體落盤

    public CachedBodyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        int contentLength = request.getContentLength();
        
        if (contentLength > 0 && contentLength <= MAX_CACHE_SIZE) {
            if (contentLength <= MEMORY_THRESHOLD) {
                this.cachedBody = readBytes(request.getInputStream());
                this.tempFile = null;
            } else {
                // 大請求體使用臨時文件
                this.tempFile = File.createTempFile("request_body_", ".tmp");
                try (InputStream in = request.getInputStream();
                     FileOutputStream out = new FileOutputStream(tempFile)) {
                    byte[] buffer = new byte[8192];
                    int len;
                    while ((len = in.read(buffer)) != -1) {
                        out.write(buffer, 0, len);
                    }
                }
                this.cachedBody = null;
            }
        } else {
            this.cachedBody = new byte[0];
            this.tempFile = null;
        }
        // 參數解析邏輯...
    }

    @Override public ServletInputStream getInputStream() {
        if (cachedBody != null) {
            return new ByteArrayServletInputStream(cachedBody);
        } else if (tempFile != null) {
            return new FileServletInputStream(tempFile);
        }
        return new ByteArrayServletInputStream(new byte[0]);
    }
}

效果驗證

場景修復前修復後
攔截器讀取 body → Controller @RequestParam參數為 null正常獲取
Filter 驗簽 → Interceptor 記錄 → Controller 業務第二個讀取異常多次讀取正常
自定義 Unicode 編碼參數解碼失敗/亂碼統一解碼正確
異步 Servlet (startAsync)流關閉異常支援異步讀取

注意事項

  1. multipart/form-data:文件上傳請求不適用此方案,需使用 MultipartFilter / StandardServletMultipartResolver
  2. 內存壓力:大量併發大請求體會增加堆內存,必須設置閾值落盤
  3. Filter 順序:必須在 CharacterEncodingFilter 之後、業務 Filter 之前
  4. 異步支援:Servlet 3.1+ 需實現 isReady()、isFinished()、setReadListener()
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved