«1. Обзор
В этом руководстве мы собираемся показать различные методы загрузки больших файлов с помощью RestTemplate.
2. RestTemplate
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.