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 .