在 Java Web 開發中,**導出文字檔(TXT/CSV)** 是常見需求。核心在於正確設定 HTTP 響應標頭、處理字元編碼、使用緩衝輸出流。本文提供兩個生產級範例:通用列表導出與 VIP 兌換碼導出,涵蓋中文檔名編碼、大數據流式寫入、資源安全釋放等關鍵細節。
text/plain; charset=UTF-8 或 text/csvattachment; filename="encodedName.txt" 觸發瀏覽器下載URLEncoder.encode(filename, "UTF-8") 解決中文檔名亂碼BufferedOutputStream + ServletOutputStream,避免大數據佔用記憶體try-with-resources (Java 7+) 或 finally 區塊確保關閉流@RequestMapping(value = "/grab/export/csv", method = RequestMethod.GET)
public void exportCsv(HttpServletRequest request, HttpServletResponse response) {
String userId = ServletRequestUtils.getStringParameter(request, "userId", "test");
// 1. 取得業務資料
List list = new ArrayList<>();
try {
list = sqlVideoList.selectSuccessDate(userId);
} catch (SQLException e) {
log.error("查詢失敗", e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
// 2. 設定響應標頭
response.setContentType("text/plain; charset=UTF-8");
String fileName = "videolist";
try {
fileName = URLEncoder.encode(fileName, "UTF-8");
} catch (UnsupportedEncodingException e) {
log.warn("檔名編碼失敗", e);
}
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + ".txt\"");
// 3. 流式寫入
try (ServletOutputStream outStr = response.getOutputStream();
BufferedOutputStream buff = new BufferedOutputStream(outStr)) {
StringBuilder write = new StringBuilder();
String enter = "\r\n"; // Windows 換行,跨平台建議 System.lineSeparator()
if (!list.isEmpty()) {
for (VideoListModel model : list) {
write.append(model.getUrl()).append(",");
write.append(model.getTitle());
write.append(enter);
}
}
// 一次性寫入(資料量大時建議分批 flush)
buff.write(write.toString().getBytes(StandardCharsets.UTF_8));
buff.flush();
} catch (IOException e) {
log.error("檔案寫入失敗", e);
// 此時響應可能已提交,無法再設定狀態碼
}
}
@RequestMapping(value = "/{exchangeId}/{packageId}/export", method = RequestMethod.GET)
public void writeToTxt(@PathVariable String exchangeId,
@PathVariable String packageId,
HttpServletRequest request,
HttpServletResponse response) throws UnsupportedEncodingException {
String schoolId = getSchoolId(request);
// 1. 查詢資料
VipCodeExample example = new VipCodeExample();
example.createCriteria()
.andSchoolIdEqualTo(schoolId)
.andPackageIdEqualTo(packageId)
.andExchangeIdEqualTo(exchangeId);
List vipCodes = vipExchangeManager.getVipCode(example);
// 2. 設定響應
response.setContentType("text/plain; charset=UTF-8");
String downloadName = vipCodes.isEmpty() ? "vip_codes" : vipCodes.get(0).getName();
String encodedName = URLEncoder.encode(downloadName, "UTF-8");
response.addHeader("Content-Disposition", "attachment; filename=\"" + encodedName + ".txt\"");
// 3. 寫入處理
try (ServletOutputStream outStr = response.getOutputStream();
BufferedOutputStream buff = new BufferedOutputStream(outStr)) {
StringBuilder write = new StringBuilder();
String tab = "\t"; // CSV 建議用逗號,這裡用 Tab 分隔
String enter = "\r\n";
for (int i = 0; i < vipCodes.size(); i++) {
VipCode code = vipCodes.get(i);
write.append(i + 1).append(tab); // 序號
write.append(code.getExchangeCode()).append(tab); // 兌換碼
write.append("normal".equals(code.getStatus()) ? "正常" : "已兌換");
write.append(enter);
}
buff.write(write.toString().getBytes(StandardCharsets.UTF_8));
buff.flush();
} catch (IOException e) {
log.error("導出失敗", e);
}
}
// 適用於百萬筆以上資料
try (ServletOutputStream out = response.getOutputStream();
BufferedOutputStream buff = new BufferedOutputStream(out, 8192)) { // 8KB 緩衝
String header = "序號\t兌換碼\t狀態\r\n";
buff.write(header.getBytes(StandardCharsets.UTF_8));
int batchSize = 1000;
for (int i = 0; i < vipCodes.size(); i += batchSize) {
int end = Math.min(i + batchSize, vipCodes.size());
StringBuilder batch = new StringBuilder();
for (int j = i; j < end; j++) {
VipCode code = vipCodes.get(j);
batch.append(j + 1).append('\t')
.append(code.getExchangeCode()).append('\t')
.append("normal".equals(code.getStatus()) ? "正常" : "已兌換")
.append('\n');
}
buff.write(batch.toString().getBytes(StandardCharsets.UTF_8));
buff.flush(); // 定期刷新,釋放記憶體
}
}
| 問題 | 原因 | 解決 |
|---|---|---|
| 中文檔名下載後亂碼 | 瀏覽器對 RFC 5987 / RFC 6266 支援不一 | 雙重編碼:filename*=UTF-8''{encoded} + filename="{fallback}" |
| 導出內容中文亂碼 | 未指定 charset 或編碼不一致 | 統一使用 StandardCharsets.UTF_8,Content-Type 含 charset |
| 大檔案導出記憶體溢位 (OOM) | 一次性載入所有資料到 StringBuilder | 分批查詢 + 分批寫入 + 適當 flush() |
| Excel 開啟 CSV 科學記號/前導零消失 | Excel 自動判斷格式 | 匯入時指定文字格式,或改用 Apache POI 導出真正 .xlsx |
Java Web 導出文字檔的關鍵在於:**正確的 HTTP 標頭、UTF-8 全鏈路編碼、流式分批寫入、資源確定性釋放**。掌握這些核心點,即可應對從簡單報表到大數據導出的各種場景。建議新專案優先使用 try-with-resources 簡化資源管理,並引入 Apache Commons CSV 或 OpenCSV 處理複雜 CSV 格式(含逗號、換行、引號義)。