web-dev-qa-db-ja.com

RestTemplateでInputStreamを取得する

URLクラスを使用して、そこからInputStreamを読み取ります。このためにRestTemplateを使用する方法はありますか?

InputStream input = new URL(url).openStream();
JsonReader reader = new JsonReader(new InputStreamReader(input, StandardCharsets.UTF_8.displayName())); 

InputStreamを使用する代わりにRestTemplateURLを取得するにはどうすればよいですか?

28
user1950349

InputStreamを直接取得しないでください。 RestTemplateは、応答(および要求)コンテンツの処理をカプセル化するためのものです。その強みは、すべてのIOを処理し、すぐに使用できるJavaオブジェクトを渡すことです。

適切な HttpMessageConverter オブジェクトを登録する必要があります。これらは、 InputStream オブジェクトを介して、応答のHttpInputMessageにアクセスできます。

Abdullが示唆するとおり 、SpringにはHttpMessageConverterResource実装が付属しており、それ自体がInputStreamをラップしています ResourceHttpMessageConverter 。すべてのResource型をサポートしているわけではありませんが、とにかくインターフェイスにプログラミングする必要があるため、スーパーインターフェイスResourceを使用するだけです。

現在の実装(4.3.5)は、応答ストリームの内容がアクセス可能な新しいByteArrayResourceにコピーされた ByteArrayInputStream を返します。

ストリームを閉じる必要はありません。 RestTemplateがそれを処理します。 (これは、InputStreamResourceでサポートされている別のタイプである ResourceHttpMessageConverter を使用しようとすると不幸です。なぜなら、基になる応答のInputStreamをラップするが、クライアントコードに公開される前に閉じられるためです。)

前の答えは間違っていませんが、私が見たいと思う深みには入りません。低レベルInputStreamを扱うことが望ましいだけでなく、必要な場合があります。最も一般的な例は、ソース(一部のWebサーバー)から宛先(データベース)に大きなファイルをストリーミングすることです。 ByteArrayInputStreamを使用しようとすると、それほど驚くことではありませんが、OutOfMemoryErrorで迎えられます。はい、独自のHTTPクライアントコードをロールできますが、誤った応答コード、応答コンバーターなどに対処する必要があります。既にSpringを使用している場合は、RestTemplateを探すのが自然な選択です。

この記事の執筆時点では、spring-web:5.0.2.RELEASEにはResourceHttpMessageConverterがあり、boolean supportsReadStreamingは、設定されていて、応答タイプがInputStreamResourceの場合、InputStreamResourceを返します。それ以外の場合は、ByteArrayResourceを返します。明らかに、ストリーミングサポートを求めたのはあなただけではありません。

ただし、問題があります。RestTemplateは、HttpMessageConverterの実行後すぐに応答を閉じます。したがって、InputStreamResourceを要求して取得したとしても、応答ストリームが閉じられているため、それは役に立ちません。これは彼らが見落としていた設計上の欠陥だと思います。応答タイプに依存しているはずです。残念ながら、読むには応答を完全に消費する必要があります。 RestTemplateを使用している場合、渡すことはできません。

しかし、書くことは問題ありません。 InputStreamをストリーミングしたい場合、ResourceHttpMessageConverterがそれを行います。内部では、org.springframework.util.StreamUtilsInputStreamからOutputStreamに一度に4096バイトを書き込みます。

一部のHttpMessageConverterはすべてのメディアタイプをサポートしているため、要件に応じて、RestTemplateからデフォルトのメディアを削除し、相対的な順序に注意して必要なメディアを設定する必要があります。

最後になりますが、ClientHttpRequestFactoryの実装にはboolean bufferRequestBody大きいストリームをアップロードする場合、falseに設定できます。また、設定する必要があります。それ以外の場合は、OutOfMemoryErrorです。この記事の執筆時点では、SimpleClientHttpRequestFactory(JDKクライアント)およびHttpComponentsClientHttpRequestFactory(Apache HTTPクライアント)はこの機能をサポートしていますが、OkHttp3ClientHttpRequestFactory。繰り返しますが、設計の監督。

編集:チケットを提出 SPR-16885 .

29
Abhijit Sarkar

Springには_org.springframework.http.converter.ResourceHttpMessageConverter_があります。 Springの_org.springframework.core.io.Resource_クラスを変換します。そのResourceクラスはInputStreamをカプセル化し、someResource.getInputStream()を介して取得できます。

これをすべてまとめると、InputStream呼び出しの応答タイプとして_Resource.class_を指定することで、すぐにRestTemplate経由でRestTemplateを取得できます。

RestTemplateexchange(..)メソッドの1つを使用した例を次に示します。

_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
_
25
Abdull

道をリードしてくれたAbhijit Sarkarの回答に感謝します。

重いJSONストリームをダウンロードし、それを小さなストリーミング可能な管理可能なデータに分割する必要がありました。 JSONは大きなプロパティを持つオブジェクトで構成されます。このような大きなプロパティはファイルにシリアル化できるため、非整列化されたJSONオブジェクトから削除できます。

別の使用例は、JSONストリームオブジェクトをオブジェクトごとにダウンロードし、map/reduceアルゴリズムのように処理し、メモリにストリーム全体をロードすることなく単一の出力を生成することです。

さらに別の使用例は、大きな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);
3
Sebien

独自の応答抽出プログラムを渡すことができます。次に、ストリーミング形式で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);
            }
    )
0
neesh

バリアントとして、応答をバイトとして消費し、ストリームに変換することができます

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());
  }
}
0
inigo skimmer