Servlet/Controller 中導出 CSV 檔案時,response.getOutputStream() 拋出 NullPointerException。本文分析常見原因並提供標準導出範本。
| 原因 | 說明 | 驗證/修正 |
|---|---|---|
| 非 Servlet 環境呼叫 | 在普通 Java 類別、單元測試、主執行緒中直接呼叫導出邏輯 | 確保在 doGet/doPost、@Controller 方法、Filter 等 Web 容器管理的執行緒中執行 |
| 回應已提交 | 在此之前已呼叫 response.getWriter()、forward()、sendRedirect()、或輸出超過緩衝區導致自動 flush | 檢查前置邏輯;導出前勿輸出任何內容;設定 response.setBufferSize(65536) |
| 框架攔截/包裝 | Spring MVC ResponseBodyAdvice、Interceptor、Shiro/權限過濾器提前提交回應 | 在導出方法上 @ResponseBody 並返回 void;或配置排除路徑 |
| 非標準 response 實作 | 測試用 MockHttpServletResponse 未正確初始化 OutputStream | 使用 MockMvc 整合測試而非單元測試 |
@GetMapping("/export.csv")
public void exportCsv(HttpServletResponse response) throws IOException {
// 1. 設置回應頭(必須在 getOutputStream() 之前)
response.setContentType("text/csv; charset=UTF-8");
response.setHeader("Content-Disposition", "attachment; filename=\"export_" + LocalDate.now() + ".csv\"");
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
// 防止中文檔名亂碼 (RFC 5987)
String encodedFilename = URLEncoder.encode("導出_" + LocalDate.now() + ".csv", StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFilename);
// 2. 獲取輸出流
try (ServletOutputStream out = response.getOutputStream();
PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
// 3. 寫入 BOM (UTF-8 with BOM) 讓 Excel 正確識別編碼
out.write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF});
// 4. 寫入 CSV 內容
writer.println("姓名,年齡,城市");
writer.println("張三,25,北京");
writer.println("李四,30,上海");
// ... 從資料庫/服務查詢數據循環寫入
writer.flush(); // 關鍵:確保緩衝區寫入 OutputStream
}
// try-with-resources 自動關閉流,提交回應
}
setHeader/setContentType 必須在 getOutputStream() / getWriter() 之前完成。getWriter(),二擇一,否則 IllegalStateException。OutputStreamWriter(..., UTF-8) 與 response.setCharacterEncoding("UTF-8") 保持一致。writer.println()。@GetMapping("/export.csv")
public ResponseEntity<byte[]> exportCsv() {
String csv = generateCsv(); // 產生 CSV 字串
byte[] bytes = ("\uFEFF" + csv).getBytes(StandardCharsets.UTF_8); // 加 BOM
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("text/csv; charset=UTF-8"));
headers.setContentDispositionFormData("attachment", "export_" + LocalDate.now() + ".csv");
headers.setCacheControl("no-cache, no-store, must-revalidate");
return new ResponseEntity<>(bytes, headers, HttpStatus.OK);
}
適用於中小數據量(< 10MB),大數據量仍建議 StreamingResponseBody 或 Servlet 直接寫流。
HttpServletResponse response?response.getWriter() / forward / redirect?getOutputStream() 前?MockMvc 而非手造 MockHttpServletResponse?遵循「Header 先行、單一流、流式寫入、編碼統一」四原則,CSV 導出穩如磐石。