HttpServletRequest.getInputStream() 只能讀取一次。實際項目中常見衝突場景:
getParameter()、@RequestParam 失效byte[] 緩存ByteArrayInputStream 包裝getParameter* 方法,從緩存數據解析參數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()));
}
}
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() {}
}
requestWrapperFilter
com.example.web.filter.RequestWrapperFilter
requestWrapperFilter
/*
REQUEST
FORWARD
INCLUDE
signatureFilter
com.example.web.filter.SignatureFilter
@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) | 流關閉異常 | 支援異步讀取 |
MultipartFilter / StandardServletMultipartResolverCharacterEncodingFilter 之後、業務 Filter 之前isReady()、isFinished()、setReadListener()