You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

响应式Spring中从DB数据生成CSV文件的Rest API实现问题

解决Spring WebFlux响应式导出CSV的问题

看起来你已经摸到正确方向了,但还有几个关键细节需要调整——既能解决406 NOT_ACCEPTABLE错误,又能高效地把Flux<Foo>转换成CSV响应,还能避免大数据量下的内存溢出问题。

核心问题分析

你遇到的406错误本质是:Spring找不到合适的消息转换器,把List<Foo>这类对象转换成text/csv格式的响应。另外,用collectList()把所有数据加载到内存再处理,完全违背了响应式编程的设计思路,数据量大时很容易触发OOM。

推荐解决方案:专业CSV库+流式输出

我建议用OpenCSV这类成熟的CSV处理库(手动拼接字符串很容易踩坑,比如字段包含逗号、引号的场景),同时直接流式输出CSV内容,不用先把所有数据攒到内存里。

第一步:引入OpenCSV依赖

先给项目添加OpenCSV的依赖(Maven示例):

<dependency>
    <groupId>com.opencsv</groupId>
    <artifactId>opencsv</artifactId>
    <version>5.6</version> <!-- 建议使用最新稳定版 -->
</dependency>

第二步:修改Controller实现流式CSV导出

下面的代码会直接从Flux<Foo>流式生成CSV内容——先输出表头,再逐行输出数据,同时设置正确的响应头让浏览器识别为可下载文件:

@RestController
@RequestMapping("/v1/foo")
@RequiredArgsConstructor
public class FooController {
    private final FooService fooService;

    @GetMapping(value = "/export", produces = MediaType.TEXT_CSV_VALUE)
    @Validated
    public Mono<ResponseEntity<Flux<String>>> export(FooQuery query) {
        // 1. 定义CSV表头
        String[] headers = {"ID", "名称", "金额", "创建时间"};
        CSVWriter headerWriter = new CSVWriter(new StringWriter());
        headerWriter.writeNext(headers);
        String headerLine = headerWriter.getWriter().toString();

        // 2. 把每个Foo对象转换成CSV行
        Flux<String> csvRows = fooService.export(query)
                .map(foo -> {
                    CSVWriter rowWriter = new CSVWriter(new StringWriter());
                    // 根据你的Foo类字段,调整这里的顺序和内容格式
                    rowWriter.writeNext(new String[]{
                            foo.getId().toString(),
                            foo.getName(),
                            foo.getAmount().toString(),
                            foo.getCreateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                    });
                    return rowWriter.getWriter().toString();
                })
                // 先输出表头,再输出数据行
                .startWith(headerLine);

        // 3. 构建响应
        return Mono.just(ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=foo_export.csv")
                .contentType(MediaType.TEXT_CSV)
                .body(csvRows));
    }
}

进阶优化:直接写入DataBuffer

如果数据量特别大,上面的方式每个对象都创建StringWriter还是有轻微开销,你可以直接写入DataBuffer,进一步降低内存占用:

@GetMapping(value = "/export", produces = MediaType.TEXT_CSV_VALUE)
@Validated
public Mono<ResponseEntity<DataBuffer>> export(FooQuery query) {
    DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
    DataBuffer buffer = bufferFactory.allocateBuffer();
    OutputStreamWriter streamWriter = new OutputStreamWriter(buffer.asOutputStream());
    CSVWriter csvWriter = new CSVWriter(streamWriter);

    // 写入表头
    csvWriter.writeNext(new String[]{"ID", "名称", "金额", "创建时间"});

    return fooService.export(query)
            .doOnNext(foo -> {
                try {
                    csvWriter.writeNext(new String[]{
                            foo.getId().toString(),
                            foo.getName(),
                            foo.getAmount().toString(),
                            foo.getCreateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
                    });
                } catch (Exception e) {
                    throw new RuntimeException("写入CSV行失败", e);
                }
            })
            .doOnComplete(() -> {
                try {
                    csvWriter.close();
                    streamWriter.close();
                } catch (IOException e) {
                    // 处理流关闭异常
                }
            })
            .then(Mono.just(ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=foo_export.csv")
                    .contentType(MediaType.TEXT_CSV)
                    .body(buffer)));
}

关键注意点

  1. 抛弃collectList():流式处理能让你边从数据库读数据,边写入CSV,无需把所有数据加载到内存,完美适配响应式场景。
  2. 设置正确的响应标识:produces = MediaType.TEXT_CSV_VALUE告诉Spring我们要返回CSV格式,Content-Disposition头让浏览器触发下载行为。
  3. 用专业库处理CSV:OpenCSV会自动处理字段中的特殊字符(比如逗号、引号),避免生成的CSV文件格式错误。

这样修改后,你的接口就能正确生成CSV文件,同时再也不会出现406错误啦。

内容的提问来源于stack exchange,提问作者Viktor M.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 07:44:17