核心需求
- Excel 開啟 CSV 不亂碼 → 必須寫入 UTF-8 BOM (EF BB BF)。
- 大數據量 (百萬列) → 流式寫入,避免 OOM。
- Web 介面下載 →
HttpServletResponse 設定正確 Header。
工具類 CSVUtils
public class CSVUtils {
// 生成 CSV 檔案 (流式寫入)
public static File createCSVFile(List<Map<String, Object>> data,
LinkedHashMap<String, String> headers, // key=欄位名, value=表頭
String outDir, String fileName) throws IOException {
File dir = new File(outDir);
if (!dir.exists()) dir.mkdirs();
File csvFile = File.createTempFile(fileName, ".csv", dir);
try (BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(csvFile), StandardCharsets.UTF_8))) {
// 寫入 UTF-8 BOM
bw.write('\uFEFF');
// 寫入表頭
boolean first = true;
for (String header : headers.values()) {
if (!first) bw.write(',');
bw.write(escapeCsv(header));
first = false;
}
bw.newLine();
// 寫入資料
for (Map<String, Object> row : data) {
first = true;
for (String field : headers.keySet()) {
if (!first) bw.write(',');
Object val = row.get(field);
bw.write(escapeCsv(val == null ? "" : val.toString()));
first = false;
}
bw.newLine();
}
}
return csvFile;
}
// CSV 轉義:包含逗號、換行、雙引號時用雙引號包裹,內部雙引號轉義為兩個雙引號
private static String escapeCsv(String s) {
if (s.contains(",") || s.contains("\n") || s.contains("\r") || s.contains("\"")) {
return "\"" + s.replace("\"", "\"\"") + "\"";
}
return s;
}
// Web 下載
public static void download(HttpServletResponse resp, File csvFile, String downloadName) throws IOException {
resp.setContentType("text/csv;charset=UTF-8");
resp.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(downloadName, "UTF-8") + "\"");
resp.setHeader("Cache-Control", "no-cache, must-revalidate");
try (InputStream in = new FileInputStream(csvFile);
OutputStream out = resp.getOutputStream()) {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) > 0) out.write(buf, 0, len);
out.flush();
}
}
// 刪除暫存檔
public static void deleteFile(File file) {
if (file != null && file.exists()) file.delete();
}
}
Controller 使用範例
@GetMapping("/export")
public void exportOrders(HttpServletResponse resp) throws IOException {
// 1. 查詢資料 (分頁/游標避免 OOM)
List<Map<String, Object>> data = orderService.exportAllOrders(); // 應使用 Stream/游標
// 2. 定義表頭順序
LinkedHashMap<String, String> headers = new LinkedHashMap<>();
headers.put("orderNo", "訂單編號");
headers.put("orderDate", "下單時間");
headers.put("amount", "金額(元)");
headers.put("status", "狀態");
// 3. 生成並下載
File csv = CSVUtils.createCSVFile(data, headers, "/tmp/export", "orders");
try {
CSVUtils.download(resp, csv, "orders_" + LocalDate.now() + ".csv");
} finally {
CSVUtils.deleteFile(csv);
}
}
大數據優化建議
- 資料庫游標/分頁:MyBatis
@Select + ResultHandler 或 Spring Data Stream<T> 逐行處理,不載入全量到記憶體。
- 非同步生成:超大檔案 (>100MB) 改為後台任務生成至物件存儲 (MinIO/S3),前端輪詢/下載連結。
- 壓縮下載:
GZIPOutputStream 包裝 resp.getOutputStream(),Header 加 Content-Encoding: gzip。