HTTP 請求由 Header 和 Body 組成。Servlet 中通過 HttpServletRequest.getReader() (字符流) 或 getInputStream() (字節流) 讀取 Body。本文提供完整工具方法,含編碼處理、大數據流式讀取、Spring MVC 替代方案與常見坑點。
public static String readBodyAsString(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = request.getReader()) { // 自動按 request 編碼解碼
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer)) != -1) {
sb.append(buffer, 0, len);
}
}
return sb.toString();
}
注意:
getReader() 和 getInputStream() **互斥**,只能調用其中之一,否則拋 IllegalStateException。Content-Type: application/json; charset=UTF-8 或在 Filter 中設置 request.setCharacterEncoding("UTF-8")。
public static byte[] readBodyAsBytes(HttpServletRequest request) throws IOException {
try (InputStream is = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
return baos.toByteArray();
}
}
// 轉字符串 (指定編碼)
String body = new String(readBodyAsBytes(request), StandardCharsets.UTF_8);
@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncBodyServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
req.startAsync();
ReadListener listener = new ReadListener() {
private final StringBuilder sb = new StringBuilder();
@Override
public void onDataAvailable() throws IOException {
try (BufferedReader reader = req.getReader()) {
char[] buf = new char[1024];
int len;
while (req.isReady() && (len = reader.read(buf)) != -1) {
sb.append(buf, 0, len);
}
}
}
@Override
public void onAllDataRead() {
String body = sb.toString();
// 處理 body
req.getAsyncContext().complete();
}
@Override
public void onError(Throwable t) {
log.error("讀取失敗", t);
req.getAsyncContext().complete();
}
};
req.getInputStream().setReadListener(listener);
}
}
| 場景 | 註解/參數 | 說明 |
|---|---|---|
| JSON 直接綁定對象 | @RequestBody UserDTO dto | 自動反序列化 (Jackson),最常用 |
| 原始 JSON 字符串 | @RequestBody String json | 獲取原始報文,用於日誌/轉發 |
| 表單數據 | @RequestParam Map | application/x-www-form-urlencoded |
| 文件上傳 | @RequestPart MultipartFile file | multipart/form-data |
| 原始 InputStream | HttpServletRequest request 參數 | 手動讀取,罕用 |
| 問題 | 原因 | 解決 |
|---|---|---|
| getReader() 返回空字串 | 流已被 Filter/Interceptor 讀取 | 使用 ContentCachingRequestWrapper 包裝請求,允許重複讀取 |
| 中文亂碼 | 編碼不一致 | Filter 中 request.setCharacterEncoding("UTF-8");客戶端發送 charset |
| 大文件 OOM | 一次性讀入內存 | 流式處理、臨時文件、分塊上傳 |
| 非阻塞讀取數據不全 | 未正確處理 isReady()/onAllDataRead() | 嚴格遵循 ReadListener 契約 |
| 雙次讀取異常 | 調用了 getReader() 再調 getInputStream() | 全鏈路統一用其中一種,或用 RequestWrapper |
public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
private final byte[] cachedBody;
public ContentCachingRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 讀取並緩存
try (InputStream is = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf)) != -1) baos.write(buf, 0, len);
cachedBody = baos.toByteArray();
}
}
@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 l) { throw new UnsupportedOperationException(); }
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream(),
StandardCharsets.UTF_8));
}
}
// Filter 中包裝
@WebFilter("/*")
public class RequestCachingFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
if (req instanceof HttpServletRequest) {
HttpServletRequest httpReq = (HttpServletRequest) req;
// 僅對有 Body 的請求包裝 (POST/PUT/PATCH)
if ("POST".equalsIgnoreCase(httpReq.getMethod())
|| "PUT".equalsIgnoreCase(httpReq.getMethod())
|| "PATCH".equalsIgnoreCase(httpReq.getMethod())) {
chain.doFilter(new ContentCachingRequestWrapper(httpReq), resp);
return;
}
}
chain.doFilter(req, resp);
}
}
request.getReader() 讀文本,getInputStream() 讀二進制,**二選一**。@RequestBody 自動綁定,原始報文用 @RequestBody String。ContentCachingRequestWrapper 包裝。