Создать CSV и вернуть как байт [] для загрузки в Spring Controller - PullRequest
1 голос
/ 04 октября 2019

Резюме: 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://stackoverflow.com/a/34508533/10327093

В обоих случаях полученный ответ выглядит как Base64. Пример (сокращенный): Q2FzZSBJRCxBc3NpZ25lZQ==

На самом деле это не Base64. Использование .body(Base64.getDecoder().decode(outputStream.toByteArray())) дает мне java.lang.IllegalArgumentException: Illegal base64 character

Если я верну void и скопирую outputStream в response.getOutputStream () с IOUtils.write(outputStream.toByteArray(), response.getOutputStream());, загруженный файл будет правильным (CSV).

Однако я хочу избежать прямого вызова response.getOutputStream (). Если впоследствии я получаю какое-либо исключение, я получаю ошибку getOutputStream() has already been called for this response в @ExceptionHandler

РЕДАКТИРОВАТЬ: Base64, декодирующий ответ, дает мне правильное значение

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

Кажется, байт [] кодируется Base64 между возвратом ResponseEntity и клиентом (пробовал с почтальоном и браузером).

Ответы [ 2 ]

0 голосов
/ 08 октября 2019

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, с ConversionService, который кодирует *От 1044 * до 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, он был удален.

0 голосов
/ 04 октября 2019

К сожалению, я не могу комментировать, поэтому я буду публиковать в качестве ответа, но на самом деле это всего лишь несколько предложений:

Вы пытались использовать "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);
}
...