Получение InputStream с помощью RestTemplate
Я использую класс URL для чтения InputStream. Есть ли способ использовать RestTemplate для этого?
InputStream input = new URL(url).openStream();
JsonReader reader = new JsonReader(new InputStreamReader(input, StandardCharsets.UTF_8.displayName()));
Как я могу получить InputStream
с RestTemplate
вместо использования URL
?
Ответы
Ответ 1
Вы не должны получать InputStream
напрямую. RestTemplate
предназначен для инкапсуляции обработки содержимого ответа (и запроса). Его сила заключается в том, чтобы обрабатывать все операции ввода-вывода и предоставлять вам готовый объект Java.
Вам нужно зарегистрировать соответствующие объекты HttpMessageConverter
. Те будут иметь доступ к ответу InputStream
через объект HttpInputMessage
.
Как предполагает Абдулл, Spring поставляется с реализацией HttpMessageConverter
для Resource
которая сама оборачивает InputStream
, ResourceHttpMessageConverter
. Он не поддерживает все типы Resource
, но, поскольку вы все равно должны программировать для интерфейсов, вам просто нужно использовать Resource
суперинтерфейса.
Текущая реализация (4.3.5) вернет ByteArrayResource
с содержимым потока ответов, скопированным в новый ByteArrayInputStream
которому вы можете получить доступ.
Вам не нужно закрывать поток. RestTemplate
позаботится об этом за вас. (Это прискорбно, если вы пытаетесь использовать InputStreamResource
, другой тип, поддерживаемый ResourceHttpMessageConverter
, потому что он оборачивает базовый ответ InputStream
но закрывается, прежде чем его можно будет открыть для вашего клиентского кода.)
Ответ 2
Предыдущие ответы не ошибаются, но они не уходят в глубину, которую мне нравится видеть. Бывают случаи, когда работа с InputStream
низкого уровня не только желательна, но и необходима, наиболее распространенным примером является потоковая передача большого файла из источника (некоторый веб-сервер) в место назначения (база данных). Если вы попытаетесь использовать ByteArrayInputStream
, вас, что не удивительно, OutOfMemoryError
с OutOfMemoryError
. Да, вы можете свернуть свой собственный код HTTP-клиента, но вам придется иметь дело с ошибочными кодами ответов, конвертерами ответов и т.д. Если вы уже используете Spring, выбор RestTemplate
является естественным выбором.
На момент написания этой статьи spring-web:5.0.2.RELEASE
имеет ResourceHttpMessageConverter
который имеет boolean supportsReadStreaming
spring-web:5.0.2.RELEASE
, которое, если установлено, и типом ответа является InputStreamResource
, возвращает InputStreamResource
; в противном случае он возвращает ByteArrayResource
. Очевидно, что вы не единственный, кто обратился за поддержкой потоковой передачи.
Однако есть проблема: RestTemplate
закрывает ответ вскоре после HttpMessageConverter
. Таким образом, даже если вы запросили InputStreamResource
и получили его, это бесполезно, поскольку поток ответов был закрыт. Я думаю, что это недостаток дизайна, который они упустили; это должно было зависеть от типа ответа. Так что, к сожалению, для чтения вы должны полностью использовать ответ; Вы не можете передать его, если используете RestTemplate
.
Письмо не проблема, хотя. Если вы хотите InputStream
поток InputStream
, ResourceHttpMessageConverter
сделает это за вас. Под капотом он использует org.springframework.util.StreamUtils
для записи 4096 байт за раз из InputStream
в OutputStream
.
Некоторые из HttpMessageConverter
поддерживают все типы носителей, поэтому, в зависимости от ваших требований, вам может потребоваться удалить стандартные по умолчанию из RestTemplate
и установить нужные, учитывая их относительное упорядочение.
Наконец, что не менее ClientHttpRequestFactory
, реализации ClientHttpRequestFactory
имеют boolean bufferRequestBody
которое можно и следует установить в значение false
если вы загружаете большой поток. В противном случае, вы знаете, OutOfMemoryError
. На момент написания этой статьи эта функция поддерживалась SimpleClientHttpRequestFactory
(клиент JDK) и HttpComponentsClientHttpRequestFactory
(клиент Apache HTTP), но не OkHttp3ClientHttpRequestFactory
. Опять же, надзор за дизайном.
Изменить: Поданный билет SPR-16885.
Ответ 3
Spring имеет org.springframework.http.converter.ResourceHttpMessageConverter
. Он преобразует класс Spring org.springframework.core.io.Resource
.
Этот класс Resource
инкапсулирует a InputStream
, который вы можете получить через someResource.getInputStream()
.
Объединяя все это, вы можете получить InputStream
через RestTemplate
из коробки, указав Resource.class
в качестве типа ответа на вызов RestTemplate
.
Вот пример использования одного из методов RestTemplate
exchange(..)
:
import org.springframework.web.client.RestTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.core.io.Resource;
ResponseEntity<Resource> responseEntity = restTemplate.exchange( someUrlString, HttpMethod.GET, someHttpEntity, Resource.class );
InputStream responseInputStream;
try {
responseInputStream = responseEntity.getBody().getInputStream();
}
catch (IOException e) {
throw new RuntimeException(e);
}
// use responseInputStream
Ответ 4
Спасибо Абхиджиту Саркару за ответ.
Мне нужно было загрузить тяжелый поток JSON и разбить его на небольшие, пригодные для обработки, фрагменты данных. JSON состоит из объектов, которые имеют большие свойства: такие большие свойства можно сериализовать в файл и, таким образом, удалить из немаршализованного объекта JSON.
Другой вариант использования - загрузка объекта потока JSON по объектам, обработка его как алгоритма отображения/сокращения и создание одного вывода без необходимости загрузки всего потока в память.
Еще один вариант использования - чтение большого файла JSON и выбор только нескольких объектов в зависимости от условия, в то же время отменяя сортировку на Plain Old Java Objects.
Вот пример: мы хотели бы передать очень большой файл JSON, который является массивом, и мы хотели бы получить только первый объект в массиве.
Учитывая этот большой файл на сервере, доступный по адресу http://example.org/testings.json:
[
{ "property1": "value1", "property2": "value2", "property3": "value3" },
{ "property1": "value1", "property2": "value2", "property3": "value3" },
... 1446481 objects => a file of 104 MB => take quite long to download...
]
Каждая строка этого массива JSON может быть проанализирована как этот объект:
@lombok.Data
public class Testing {
String property1;
String property2;
String property3;
}
Вам нужен этот класс для многократного использования кода синтаксического анализа:
import com.fasterxml.jackson.core.JsonParser;
import java.io.IOException;
@FunctionalInterface
public interface JsonStreamer<R> {
/**
* Parse the given JSON stream, process it, and optionally return an object.<br>
* The returned object can represent a downsized parsed version of the stream, or the result of a map/reduce processing, or null...
*
* @param jsonParser the parser to use while streaming JSON for processing
* @return the optional result of the process (can be {@link Void} if processing returns nothing)
* @throws IOException on streaming problem (you are also strongly encouraged to throw HttpMessageNotReadableException on parsing error)
*/
R stream(JsonParser jsonParser) throws IOException;
}
И этот класс разобрать
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonParser;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
@AllArgsConstructor
public class StreamingHttpMessageConverter<R> implements HttpMessageConverter<R> {
private final JsonFactory factory;
private final JsonStreamer<R> jsonStreamer;
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return MediaType.APPLICATION_JSON.isCompatibleWith(mediaType);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false; // We only support reading from an InputStream
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Collections.singletonList(MediaType.APPLICATION_JSON);
}
@Override
public R read(Class<? extends R> clazz, HttpInputMessage inputMessage) throws IOException {
try (InputStream inputStream = inputMessage.getBody();
JsonParser parser = factory.createParser(inputStream)) {
return jsonStreamer.stream(parser);
}
}
@Override
public void write(R result, MediaType contentType, HttpOutputMessage outputMessage) {
throw new UnsupportedOperationException();
}
}
Далее приведен код, который нужно использовать для потоковой передачи ответа HTTP, анализа массива JSON и возврата только первого не маршализованного объекта:
// You should @Autowire these:
JsonFactory jsonFactory = new JsonFactory();
ObjectMapper objectMapper = new ObjectMapper();
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder();
// If detectRequestFactory true (default): HttpComponentsClientHttpRequestFactory will be used and it will consume the entire HTTP response, even if we close the stream early
// If detectRequestFactory false: SimpleClientHttpRequestFactory will be used and it will close the connection as soon as we ask it to
RestTemplate restTemplate = restTemplateBuilder.detectRequestFactory(false).messageConverters(
new StreamingHttpMessageConverter<>(jsonFactory, jsonParser -> {
// While you use a low-level JsonParser to not load everything in memory at once,
// you can still profit from smaller object mapping with the ObjectMapper
if (!jsonParser.isClosed() && jsonParser.nextToken() == JsonToken.START_ARRAY) {
if (!jsonParser.isClosed() && jsonParser.nextToken() == JsonToken.START_OBJECT) {
return objectMapper.readValue(jsonParser, Testing.class);
}
}
return null;
})
).build();
final Testing firstTesting = restTemplate.getForObject("http://example.org/testings.json", Testing.class);
log.debug("First testing object: {}", firstTesting);
Ответ 5
Вы можете передать в свой собственный экстрактор ответа. Вот пример, где я записываю json на диск в потоковом режиме -
RestTemplate restTemplate = new RestTemplateBuilder().basicAuthentication("user", "their_password" ).build();
int responseSize = restTemplate.execute(uri,
HttpMethod.POST,
(ClientHttpRequest requestCallback) -> {
requestCallback.getHeaders().setContentType(MediaType.APPLICATION_JSON);
requestCallback.getBody().write(body.getBytes());
},
responseExtractor -> {
FileOutputStream fos = new FileOutputStream(new File("out.json"));
return StreamUtils.copy(responseExtractor.getBody(), fos);
}
)
Ответ 6
В качестве варианта вы можете использовать ответ как байты, а не конвертировать в поток
byte data[] = restTemplate.execute(link, HttpMethod.GET, null, new BinaryFileExtractor());
return new ByteArrayInputStream(data);
Экстрактор
public class BinaryFileExtractor implements ResponseExtractor<byte[]> {
@Override
public byte[] extractData(ClientHttpResponse response) throws IOException {
return ByteStreams.toByteArray(response.getBody());
}
}