响应式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))); }
关键注意点
- 抛弃
collectList():流式处理能让你边从数据库读数据,边写入CSV,无需把所有数据加载到内存,完美适配响应式场景。 - 设置正确的响应标识:
produces = MediaType.TEXT_CSV_VALUE告诉Spring我们要返回CSV格式,Content-Disposition头让浏览器触发下载行为。 - 用专业库处理CSV:OpenCSV会自动处理字段中的特殊字符(比如逗号、引号),避免生成的CSV文件格式错误。
这样修改后,你的接口就能正确生成CSV文件,同时再也不会出现406错误啦。
内容的提问来源于stack exchange,提问作者Viktor M.
相关产品推荐
相关产品推荐

