Получение 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());
  }
}