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 公式注入

如果导出内容来自用户输入,以 =, +, -, @ 开头的单元格可能被表格软件当作公式执行。对不可信文本应转义或增加安全前缀。