Перейти к основному содержимому

Загрузите большой файл через Spring RestTemplate

· 3 мин. чтения

1. Обзор

В этом уроке мы собираемся показать различные методы загрузки больших файлов с помощью RestTemplate .

2. Шаблон отдыха

RestTemplate — это блокирующий и синхронный HTTP-клиент, представленный в Spring 3. Согласно документации Spring , в будущем он будет объявлен устаревшим, поскольку они представили WebClient в качестве реактивного неблокирующего HTTP-клиента в версии 5.

3. Подводные камни

Обычно, когда мы загружаем файл, мы сохраняем его в нашей файловой системе или загружаем в память в виде массива байтов. Но когда это большой файл, загрузка в память может привести к ошибке OutOfMemoryError . Следовательно, мы должны хранить данные в файле, когда мы читаем фрагменты ответа.

Давайте сначала рассмотрим пару способов, которые не работают:

Во-первых, что произойдет, если мы вернем Resource в качестве возвращаемого типа:

Resource download() {
  return new ClassPathResource(locationForLargeFile);
}

Причина, по которой это не работает, заключается в том, что ResourceHttpMesssageConverter загрузит все тело ответа в ByteArrayInputStream , по-прежнему добавляя нехватку памяти, которой мы хотели избежать.

Во- вторых, что, если мы вернем InputStreamResource и настроим ResourceHttpMessageConverter#supportsReadStreaming ? Что ж, это тоже не работает, поскольку к тому времени, когда мы можем вызвать InputStreamResource.getInputStream() , мы получаем ошибку « сокет закрыт» ! Это связано с тем, что «выполнение » закрывает входной поток ответа перед выходом.

Итак, что мы можем сделать, чтобы решить проблему? Собственно, здесь тоже две вещи:

  • Напишите собственный HttpMessageConverter , который поддерживает File в качестве возвращаемого типа.
  • Используйте RestTemplate.execute с пользовательским ResponseExtractor для сохранения входного потока в файле .

В этом уроке мы будем использовать второе решение, потому что оно более гибкое и требует меньше усилий.

4. Скачать без возобновления

Давайте реализуем ResponseExtractor для записи тела во временный файл :

File file = restTemplate.execute(FILE_URL, HttpMethod.GET, null, clientHttpResponse -> {
File ret = File.createTempFile("download", "tmp");
StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(ret));
return ret;
});

Assert.assertNotNull(file);
Assertions
.assertThat(file.length())
.isEqualTo(contentLength);

Здесь мы использовали StreamUtils.copy для копирования входного потока ответа в FileOutputStream, но также доступны другие методы и библиотеки .

5. Загрузка с паузой и возобновлением

Поскольку мы собираемся загрузить большой файл, разумно подумать о загрузке после того, как мы по какой-то причине остановились.

Итак, сначала давайте проверим, поддерживает ли URL загрузки возобновление:

HttpHeaders headers = restTemplate.headForHeaders(FILE_URL);

Assertions
.assertThat(headers.get("Accept-Ranges"))
.contains("bytes");
Assertions
.assertThat(headers.getContentLength())
.isGreaterThan(0);

Затем мы можем реализовать RequestCallback , чтобы установить заголовок «Range» и возобновить загрузку:

restTemplate.execute(
FILE_URL,
HttpMethod.GET,
clientHttpRequest -> clientHttpRequest.getHeaders().set(
"Range",
String.format("bytes=%d-%d", file.length(), contentLength)),
clientHttpResponse -> {
StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(file, true));
return file;
});

Assertions
.assertThat(file.length())
.isLessThanOrEqualTo(contentLength);

Если мы не знаем точную длину содержимого, мы можем установить значение заголовка Range с помощью String.format :

String.format("bytes=%d-", file.length())

6. Заключение

Мы обсудили проблемы, которые могут возникнуть при загрузке большого файла. Мы также представили решение при использовании RestTemplate . Наконец, мы показали, как реализовать возобновляемую загрузку.

Как всегда код доступен на нашем GitHub .