CSV 本質與優勢
- 純文本,逗號分隔,Excel 可直接開啟
- 無 POI 對象樹開銷,記憶體佔用極低,適合大數據導出
- 流式寫入,邊查邊寫,避免 OOM
通用導出工具類
public class CsvUtil {
public static void exportCsv(
String[] headers, // 表頭
String[] fields, // 實體屬性名,與 headers 一一對應
List dataList, // 數據源
OutputStream out, // 輸出流(HttpServletResponse.getOutputStream())
String charset // 建議 "GBK" 兼容中文 Excel
) throws IOException, IllegalAccessException {
try (OutputStreamWriter writer = new OutputStreamWriter(out, charset)) {
// 1. 寫表頭
for (int i = 0; i < headers.length; i++) {
writer.write(headers[i]);
if (i < headers.length - 1) writer.write(',');
}
writer.write("\r\n");
// 2. 反射寫數據
for (T obj : dataList) {
for (int i = 0; i < fields.length; i++) {
String fieldName = fields[i];
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object val = field.get(obj);
writer.write(val != null ? val.toString() : "");
} catch (NoSuchFieldException e) {
writer.write(""); // 屬性不存在留空
}
if (i < fields.length - 1) writer.write(',');
}
writer.write("\r\n");
}
writer.flush();
}
}
}
Controller 整合範例
@GetMapping("/export/users.csv")
public void exportUsers(HttpServletResponse response) throws IOException {
List users = userService.findAll(); // 或分頁流式查詢
response.setContentType("text/csv;charset=GBK");
response.setHeader("Content-Disposition", "attachment;filename=users_" + System.currentTimeMillis() + ".csv");
String[] headers = {"ID", "用戶名", "郵箱", "建立時間"};
String[] fields = {"id", "username", "email", "createTime"};
CsvUtil.exportCsv(headers, fields, users, response.getOutputStream(), "GBK");
}
百萬級優化建議
- 流式查詢:MyBatis
@Select + ResultHandler 或 Cursor,避免一次性載入 List
- 分批寫入:每 1000 條 flush 一次,減少 GC 壓力
- 異步導出:大文件提交任務 → 後台生成 → WebSocket/郵件通知下載
- 字段預編譯:啟動時緩存
Field[],避免每行反射 getDeclaredField
常見坑點
- 字段包含逗號/換行/雙引號 → 需用雙引號包裹並轉義雙引號(RFC 4180)
- Excel 打開 CSV 自動科學計數法(長數字/身份證) → 字段前加
\t 或改用 xlsx
- UTF-8 BOM 導致 Excel 亂碼 → 輸出流寫入
new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF} 或改用 GBK