1. Обзор
В этом уроке мы покажем различные методы, как загружать большие файлы с помощью RestTemplate .
2. RestTemplate
RestTemplate – это блокирующий и асинхронный HTTP-клиент, представленный в Spring 3. Согласно документации Spring , в будущем он будет устарел, так как они ввели WebClient | в качестве реактивного неблокирующего HTTP-клиента в версии 5.
3. Подводные камни
Обычно, когда мы загружаем файл, мы сохраняем его в нашей файловой системе или загружаем в память в виде массива байтов. Но когда это большой файл, загрузка в память может привести к OutOfMemoryError . Следовательно, мы должны хранить данные в файле, когда читаем фрагменты ответа.
Давайте сначала рассмотрим несколько способов, которые не работают:
Во-первых, что произойдет, если мы вернем Resource в качестве типа возвращаемого значения:
Resource download() { return new ClassPathResource(locationForLargeFile); }
Причина, по которой это не работает, заключается в том, что Resourcehttpmessageconverter загрузит все тело ответа в ByteArrayInputStream | все еще добавляя давление памяти, которого мы хотели избежать.
Во-вторых, что делать, если мы вернем InputStreamResource и настроим ResourceHttpMessageConverter#supportsReadStreaming ? Ну, это тоже не работает, так как к тому времени, когда мы сможем позвонить InputStreamResource.getInputStream() , мы получаем “ сокет закрыт” ошибка! Это происходит потому, что “execute ” закрывает входной поток ответа перед выходом.
Так что же мы можем сделать, чтобы решить эту проблему? На самом деле, здесь тоже есть две вещи:
- Напишите обычай HttpMessageConverter который поддерживает File в качестве возвращаемого типа
- Используйте RestTemplate.execute with a custom 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);
Затем мы можем реализовать Запрос обратного вызова для установки заголовка “Диапазон” и возобновления загрузки:
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 .