私はWebClient
とカスタムBodyExtractor
classをスプリングブートアプリケーションに使用しています
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のトリックはあまり知りません。
Flux#collect
とSequenceInputStream
を使用して機能させることができました
@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;
}
}
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
を初期値として作成することもできます。
これは、他の答えが示唆するほど複雑ではありません。
すべてをメモリにバッファリングせずにデータをストリーミングする唯一の方法は、@ 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()))
_
代わりに。
InputStream
操作が完了するまで何も出力されないため、WebClient
を再構築すると、そもそもcollect
を使用する目的が無効になります。大規模なストリームでは、非常に長い時間がかかる場合があります。リアクティブモデルは個々のバイトを処理するのではなく、バイトのブロック(Spring DataBuffer
など)を処理します。よりエレガントな解決策については、私の回答を参照してください: https://stackoverflow.com/a/48054615/8397
パイプを使用できます。
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());
}