web-dev-qa-db-ja.com

Flux <DataBuffer>を正しく読み取り、単一のinputStreamに変換する方法

私はWebClientとカスタムBodyExtractorclassをスプリングブートアプリケーションに使用しています

WebClient webLCient = WebClient.create();
webClient.get()
   .uri(url, params)
   .accept(MediaType.APPLICATION.XML)
   .exchange()
   .flatMap(response -> {
     return response.body(new BodyExtractor());
   })

BodyExtractor.Java

@Override
public Mono<T> extract(ClientHttpResponse response, BodyExtractor.Context context) {
  Flux<DataBuffer> body = response.getBody();
  body.map(dataBuffer -> {
    try {
      JaxBContext jc = JaxBContext.newInstance(SomeClass.class);
      Unmarshaller unmarshaller = jc.createUnmarshaller();

      return (T) unmarshaller.unmarshal(dataBuffer.asInputStream())
    } catch(Exception e){
       return null;
    }
  }).next();
}

上記のコードは小さなペイロードで機能しますが、大きなペイロードでは機能しません。単一のフラックス値をnextで読み取るだけで、すべてのdataBufferを組み合わせて読み取る方法がわからないためだと思います。

私はリアクターが初めてなので、flux/monoのトリックはあまり知りません。

11
Bk Santiago

Flux#collectSequenceInputStreamを使用して機能させることができました

@Override
public Mono<T> extract(ClientHttpResponse response, BodyExtractor.Context context) {
  Flux<DataBuffer> body = response.getBody();
  return body.collect(InputStreamCollector::new, (t, dataBuffer)-> t.collectInputStream(dataBuffer.asInputStream))
    .map(inputStream -> {
      try {
        JaxBContext jc = JaxBContext.newInstance(SomeClass.class);
        Unmarshaller unmarshaller = jc.createUnmarshaller();

        return (T) unmarshaller.unmarshal(inputStream);
      } catch(Exception e){
        return null;
      }
  }).next();
}

InputStreamCollector.Java

public class InputStreamCollector {
  private InputStream is;

  public void collectInputStream(InputStream is) {
    if (this.is == null) this.is = is;
    this.is = new SequenceInputStream(this.is, is);
  }

  public InputStream getInputStream() {
    return this.is;
  }
}
2
Bk Santiago

Bk Santiagoの答えを少し修正したバージョンでは、reduce()の代わりにcollect()を使用しています。非常に似ていますが、追加のクラスを必要としません:

Java:

_body.reduce(new InputStream() {
    public int read() { return -1; }
  }, (s: InputStream, d: DataBuffer) -> new SequenceInputStream(s, d.asInputStream())
).flatMap(inputStream -> /* do something with single InputStream */
_

またはコトリン:

_body.reduce(object : InputStream() {
  override fun read() = -1
}) { s: InputStream, d -> SequenceInputStream(s, d.asInputStream()) }
  .flatMap { inputStream -> /* do something with single InputStream */ }
_

collect()を使用するよりもこのアプローチの利点は、単に物事をまとめるために別のクラスを用意する必要がないことです。

新しい空のInputStream()を作成しましたが、その構文がわかりにくい場合は、代わりにByteArrayInputStream("".toByteArray())に置き換えて、代わりに空のByteArrayInputStreamを初期値として作成することもできます。

4
samanime

これは、他の答えが示唆するほど複雑ではありません。

すべてをメモリにバッファリングせずにデータをストリーミングする唯一の方法は、@ jin-kwonが示唆したように、パイプを使用することです。ただし、Springの BodyExtractors および DataBufferUtils ユーティリティクラスを使用すると、非常に簡単に実行できます。

例:

_private InputStream readAsInputStream(String url) throws IOException {
    PipedOutputStream osPipe = new PipedOutputStream();
    PipedInputSteam isPipe = new PipedInputStream(osPipe);

    ClientResponse response = webClient.get().uri(url)
        .accept(MediaType.APPLICATION.XML)
        .exchange()
        .block();
    final int statusCode = response.rawStatusCode();
    // check HTTP status code, can throw exception if needed
    // ....

    Flux<DataBuffer> body = response.body(BodyExtractors.toDataBuffers())
        .doOnError(t -> {
            log.error("Error reading body.", t);
            // close pipe to force InputStream to error,
            // otherwise the returned InputStream will hang forever if an error occurs
            try(isPipe) {
              //no-op
            } catch (IOException ioe) {
                log.error("Error closing streams", ioe);
            }
        })
        .doFinally(s -> {
            try(osPipe) {
              //no-op
            } catch (IOException ioe) {
                log.error("Error closing streams", ioe);
            }
        });

    DataBufferUtils.write(body, osPipe)
        .subscribe(DataBufferUtils.releaseConsumer());

    return isPipe;
}
_

応答コードの確認やエラーステータスコードの例外のスローを気にしない場合は、block()呼び出しと中間のClientResponse変数を使用してスキップできます。

_flatMap(r -> r.body(BodyExtractors.toDataBuffers()))
_

代わりに。

0
user1585916

InputStream操作が完了するまで何も出力されないため、WebClientを再構築すると、そもそもcollectを使用する目的が無効になります。大規模なストリームでは、非常に長い時間がかかる場合があります。リアクティブモデルは個々のバイトを処理するのではなく、バイトのブロック(Spring DataBufferなど)を処理します。よりエレガントな解決策については、私の回答を参照してください: https://stackoverflow.com/a/48054615/8397

0
Abhijit Sarkar

パイプを使用できます。

public static <R> R pipeBodyToStreamAndApply(
        final int pipeSize,
        final Flux<DataBuffer> dataBuffers,
        final Executor taskExecutor,
        final Function<? super InputStream, ? extends R> streamFunction)
        throws IOException {
    final PipedOutputStream output = new PipedOutputStream();
    final PipedInputStream input = new PipedInputStream(output, pipeSize);
    final Flux<DataBuffer> mapped = dataBuffers.map(b -> {
        final byte[] d = new byte[b.readableByteCount()];
        b.read(d);
        try {
            output.write(d);
        } catch (final IOException ioe) {
            throw new RuntimeException("failed to write", ioe);
        }
        return b;
    });
    taskExecutor.execute(() -> {
        flux.map(DataBufferUtils::release).blockLast();
        try {
            output.flush();
            output.close();
        } catch (final IOException ioe) {
            throw new RuntimeException(
                "failed to flush and close the piped output stream", ioe);
        }
    });
    return streamFunction.apply(input);
}

そして、ここにチャンネルがあります。

public static <R> R pipeBodyToChannelAndApply(
        final Flux<DataBuffer> dataBuffers,
        final Executor taskExecutor,
        final Function<? super ReadableByteChannel, ? extends R> channelFunction)
        throws IOException {
    final Pipe pipe = Pipe.open();
    final Flux<DataBuffer> flux = dataBuffers.map(b -> {
        for (final ByteBuffer s = b.asByteBuffer(); s.hasRemaining(); ) {
            try {
                final int written = pipe.sink().write(s);
            } catch (final IOException ioe) {
                throw new RuntimeException("failed to write", ioe);
            }
        }
        return b;
    });
    taskExecutor.execute(() -> {
        flux.map(DataBufferUtils::release).blockLast();
        try {
            pipe.sink().close();
        } catch (final IOException ioe) {
            throw new RuntimeException("failed to close the pipe.sink", ioe);
        }
    });
    return channelFunction.apply(pipe.source());
}
0
Jin Kwon