Super CSV 可以通过 Bean 字段映射和 CellProcessor 完成格式转换,适合从 Web 接口流式导出 CSV。
导出流程
完整流程可以拆成五步:
- 设置下载响应头和 UTF-8 编码。
- 定义列处理器、展示表头和 Bean 字段映射。
- 在内容最前面写入 UTF-8 BOM。
- 创建
CsvBeanWriter并写入表头。 - 分页查询数据并持续写入响应流。
代码示例
public void exportChannelBalanceHistory(
@RequestBody ChannelBalanceHistoryQueryDTO queryDTO,
HttpServletResponse response
) throws IOException {
String fileName = "ChannelBalanceHistory_" + DateUtil.getformatDDMMYYYY() + ".csv";
response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
response.setHeader(
"Content-Disposition",
"attachment; filename=\"" + fileName + "\""
);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("text/csv;charset=UTF-8");
String language = Context.getLanguage();
CellProcessor[] processors = new CellProcessor[]{
new FmtDate("dd/MM/yyyy"),
new Optional(),
new Optional(),
new Optional(),
new Optional(),
new Optional(),
new Optional(new CellProcessorAdaptor() {
@Override
@SuppressWarnings("unchecked")
public <T> T execute(Object value, CsvContext context) {
if (value instanceof Integer) {
boolean enabled = ((Integer) value) == 1;
String result = Objects.equals(language, Context.DEFAULT_LANGUAGE)
? (enabled ? "启动" : "关闭")
: (enabled ? "Open" : "Closed");
return (T) result;
}
return (T) value;
}
})
};
String[] header = Objects.equals(language, Context.DEFAULT_LANGUAGE)
? new String[]{"日期", "运营商", "币种", "账户余额", "预警余额", "预警邮件接收邮箱", "状态"}
: new String[]{"Balance Date", "Operator", "Currency", "Balance USD", "Warning Amount", "Email", "Status"};
String[] nameMapping = {
"balanceDate",
"channelName",
"currency",
"balance",
"warnBalance",
"warnMails",
"doneWarn"
};
PrintWriter writer = response.getWriter();
// Excel 打开包含中文的 UTF-8 CSV 时,可通过 BOM 正确识别编码。
writer.write('\uFEFF');
try (CsvBeanWriter csvWriter =
new CsvBeanWriter(writer, CsvPreference.STANDARD_PREFERENCE)) {
csvWriter.writeHeader(header);
int pageNo = 1;
final int pageSize = 1000;
PageInfoDTO<ChannelBalanceHistoryRespDTO> page;
do {
queryDTO.setPageNo(pageNo++);
queryDTO.setPageSize(pageSize);
page = bossRechargeOrderHandlerFacade.queryChannelBalanceHistory(queryDTO);
for (ChannelBalanceHistoryRespDTO item : page.getList()) {
csvWriter.write(item, nameMapping, processors);
}
} while (page.getList().size() == pageSize);
}
}
容易踩坑的地方
BOM 必须先于 CSV 内容写入
UTF-8 BOM 的字节序列是 EF BB BF,也可以直接通过 writer.write('\uFEFF') 写入。必须先写 BOM,再创建并使用 CsvBeanWriter,否则 BOM 可能出现在首个单元格中。
不要混用 Writer 和 OutputStream
同一个 HttpServletResponse 不能同时调用 getWriter() 和 getOutputStream()。决定使用字符流后,BOM 和 CSV 内容都应写入同一个 Writer。
表头与字段映射必须一一对应
header 决定用户看到的列名,nameMapping 决定从 Bean 读取哪个属性,processors 决定如何转换该值。三组数组的长度和顺序必须保持一致。
分页导出要有稳定顺序
数据库查询应指定稳定且唯一的排序条件,避免翻页期间出现重复或遗漏。数据量非常大时,还应考虑游标/键集分页,避免深分页越来越慢。
防止 CSV 公式注入
如果导出内容来自用户输入,以 =, +, -, @ 开头的单元格可能被表格软件当作公式执行。对不可信文本应转义或增加安全前缀。