创建一个 CSV 并返回为 byte[] 以在 Spring Controller 中下载

Posted

技术标签:

【中文标题】创建一个 CSV 并返回为 byte[] 以在 Spring Controller 中下载【英文标题】:Create a CSV and return as byte[] for download in a Spring Controller 【发布时间】:2020-02-02 07:00:05 【问题描述】:

总结:ResponseEntity<byte[]> 包含 CSV 不会返回为 CSV


我正在为下载按钮编写一个控制器,它将生成一个 CSV,其中包含从另一个依赖项中检索到的数据。

    @PostMapping(BASE_ROUTE + "/download")
    public ResponseEntity<byte[]> downloadCases(@RequestBody RequestType request, final HttpServletResponse response) throws IOException 
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
                OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
                PrintWriter writer = new PrintWriter(outputStreamWriter, true)) 
            String nextPageToken = null;
            do 
                ServiceResponse serviceResponse = makeServiceCall(request.getParam());
                // write a String of comma separated values
                writer.println(serviceResponse.getLine());
                // eventually null
                nextPageToken = serviceResponse.getToken(); 
             while (nextPageToken != null);

            return ResponseEntity.ok()
                    .header(HttpHeaders.CONTENT_TYPE, "text/csv")
                    .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.csv\"")
                    .body(outputStream.toByteArray());
        

作为测试,我还尝试将正文设置为.body("Case ID,Assignee".getBytes(StandardCharsets.UTF_8))。这应该等同于https://***.com/a/34508533/10327093

在这两种情况下,我得到的响应都类似于 Base64。示例(缩短):Q2FzZSBJRCxBc3NpZ25lZQ==

它似乎实际上不是 Base64。使用.body(Base64.getDecoder().decode(outputStream.toByteArray())) 给我java.lang.IllegalArgumentException: Illegal base64 character

如果我返回 void 并使用 IOUtils.write(outputStream.toByteArray(), response.getOutputStream()); 将 outputStream 复制到 response.getOutputStream(),则下载的文件是正确的 (CSV)。

但是我想避免直接调用 response.getOutputStream()。如果之后我得到任何异常,我会在@ExceptionHandler 中得到错误getOutputStream() has already been called for this response

编辑:Base64 解码响应给我正确的值

System.out.println(new String(Base64.getDecoder().decode("Q2FzZSBJRCxBc3NpZ25lZQ==")));
// Case ID,Assignee

似乎 byte[] 在返回 ResponseEntity 和客户端之间进行了 Base64 编码(使用 Postman 和浏览器进行了尝试)。

【问题讨论】:

【参考方案1】:

很遗憾,我无法发表评论,所以我将发布作为答案,但这只是一些建议:

你试过用“application/csv”代替“text”/csv吗?

不久前我不得不做一些非常相似的事情,只看你的代码,我没有发现任何问题。

这是我所做的一个例子:

@GetMapping("/" , produces="application/vnd.ms-excel")
public ResponseEntity<Object> myMethod() 
  String filename = "filename.xlsx";
  byte[] bytes = getBytes();
  HttpHeaders headers = new HttpHeaders();
  headers.set("Content-Type", "application/vnd.ms-excel;");
  headers.setContentDispositionFormData(filename, filename);
  return new ResponseEntity<Object>(bytes, headers, HttpStatus.OK);

【讨论】:

我试过了,但仍然从浏览器和 Postman 获得 Base64 也来自邮递员?您如何尝试将文件保存在 Postman 上?您是否点击“发送和下载”(***.com/questions/38975718/…)? 如果将“produces”属性添加到 GetMapping 会发生什么?我编辑了我的答案。【参考方案2】:

TL;DR:不要以byte[] 的形式提供数据,而是以String 的形式提供数据。


当使用ResponseEntity 时,Spring 使用已注册的HttpMessageConverter

如果您指定内容类型为application/octet-stream,正文类型为byte[],Spring 将使用ByteArrayHttpMessageConverter 并按原样发送字节。

如果您指定 text/* 的内容类型,例如您指定的text/csv,具有String 的主体类型,Spring 将使用StringHttpMessageConverter,它将适当地编码文本并发送它。最好明确指定charset,例如使用内容类型text/csv; charset=UTF-8,因此客户端知道服务器使用的字符集。

由于您指定了 text/csv 的内容类型,但正文类型为 byte[],因此 Spring 正在使用其他一些 HttpMessageConverter,可能是 ObjectToStringHttpMessageConverter,以及将 byte[] 编码为的 ConversionService String 作为 Base-64。

旁注:为了获得更好的性能,不要自动刷新每一行输出。关闭自动刷新并在完成后执行最终的flush()

长话短说,既然您需要文字,请不要转换为byte[]。使用StringWriter 将所有内容保留为文本:

@PostMapping(path = BASE_ROUTE + "/download", produces = "text/csv; charset=UTF-8")
public ResponseEntity<String> downloadCases(@RequestBody RequestType request) throws IOException 
    try (StringWriter stringWriter = new StringWriter();
            PrintWriter writer = new PrintWriter(stringWriter)) 
        String nextPageToken = null;
        do 
            ServiceResponse serviceResponse = makeServiceCall(request.getParam());
            // write a String of comma separated values
            writer.println(serviceResponse.getLine());
            // eventually null
            nextPageToken = serviceResponse.getToken();
         while (nextPageToken != null);
        writer.flush();

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_TYPE, "text/csv; charset=UTF-8")
                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"report.csv\"")
                .body(stringWriter.toString());
    

由于您从未使用过 response 参数,因此已将其删除。

【讨论】:

我尝试了application/octet-stream,但仍然得到了 Base64。我开始怀疑它是否与使用 POST 而不是 GET 有关?更改为 String 有效。也就是说,我在 *** 上看到的大多数示例都使用 byte[]。在 byte[] 上使用 String 有什么缺点吗?还是仅仅是因为大多数示例都是从可能不是文本的文件中读取的?

以上是关于创建一个 CSV 并返回为 byte[] 以在 Spring Controller 中下载的主要内容,如果未能解决你的问题,请参考以下文章

ParserError: NULL byte detected. This byte cannot be processed in Python‘s native csv library(

将大型 csv 转换为稀疏矩阵以在 sklearn 中使用

Linq to CSV:将数据转换为 .CSV 文件并从操作中返回文件,而不在服务器上保存文件

python 格式化单个值以在符合RFC4180的CSV文件中使用。使用逗号将这个函数返回的多个值连接到crea

将 Scala 中的任何类型转换为 Array[Byte] 并返回

如何将 byte[] 转换为 float[] 并返回 Java?