如何正确读取Flux <DataBuffer>并将其转换为单个inputStream

Bk *_*ago 12 java spring project-reactor reactive-streams spring-webflux

我正在为我的spring-boot应用程序使用WebClient和自定义BodyExtractor类

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

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();
}
Run Code Online (Sandbox Code Playgroud)

上面的代码使用小的有效负载而不是大的有效负载,我认为这是因为我只读取一个通量值,next我不知道如何组合和读取所有dataBuffer.

我是反应堆的新手,所以我不知道很多使用flux/mono的技巧.

use*_*916 18

这真的没有其他答案暗示的那么复杂。

正如@jin-kwon 建议的那样,流式传输数据而不将其全部缓存在内存中的唯一方法是使用管道。但是,它可以通过使用 Spring 的BodyExtractors和DataBufferUtils实用程序类非常简单地完成。

例子:

private InputStream readAsInputStream(String url) throws IOException {
    PipedOutputStream osPipe = new PipedOutputStream();
    PipedInputStream 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;
}
Run Code Online (Sandbox Code Playgroud)

如果您不关心检查响应代码或为失败状态代码抛出异常,则可以通过使用跳过block()调用和中间ClientResponse变量

flatMap(r -> r.body(BodyExtractors.toDataBuffers()))
Run Code Online (Sandbox Code Playgroud)

反而。

  • 使用reactor-core 3.3.9.RELEASE 无法使用Java 8。PipedInputStream 和 PipedOutputStream 仅包含 0,没有终止符。它将我的解组器挂在调用 unmarshaller.unmarshal(isPipe) 中。事实上,在我的调试器中, doFinally 永远不会被调用,这是可疑的 (3认同)
  • 将“PipedInputSteam”更改为“PipedInputStream”,将“MediaType.APPLICATION.XML”更改为“MediaType.APPLICATION_XML”。我摆脱了状态代码,所以我需要使用 `flatMapMany(r -&gt; r.body(BodyExtractors.toDataBuffers()))` 而不是 `flatMap(r -&gt; r.body(BodyExtractors.toDataBuffers()))` (2认同)

Jin*_*won 7

这是其他答案的另一种变体。而且它仍然不是内存友好的。

static Mono<InputStream> asStream(WebClient.ResponseSpec response) {
    return response.bodyToFlux(DataBuffer.class)
        .map(b -> b.asInputStream(true))
        .reduce(SequenceInputStream::new);
}

static void doSome(WebClient.ResponseSpec response) {
    asStream(response)
        .doOnNext(stream -> {
            // do some with stream
            // close the stream!!!
        })
        .block();
}
Run Code Online (Sandbox Code Playgroud)

  • 当心。如果你关闭 SequenceInputStream(你应该这样做,否则你会从 Netty 得到未释放的缓冲区错误),那么如果你有一个大文件或很多小缓冲区,它很容易导致 StackoverflowError。 (2认同)

Bk *_*ago 5

我能够通过使用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();
}
Run Code Online (Sandbox Code Playgroud)

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 为什么要编写自己的 BodyExtractor?WebFlux 已经通过 Jaxb2XmlDecoder 支持 Jaxb。 (3认同)
  • 这个解决方案不是将所有响应体读入内存吗?`ByteBuffer` 将所有数据存储在内存中,对吗?因此生成的 `InputStream` 将与 `ByteArrayInputStream` 相同,因此该解决方案不处理大数据。 (3认同)
  • 有趣,但`WebClient` 是这个工作的错误工具。您正在重构响应 `InputStream`,因此使用 `WebClient` 没有任何优势。您最好使用普通的普通 HTTP 客户端。 (2认同)

sam*_*ime 5

A slightly modified version of Bk Santiago's answer makes use of reduce() instead of collect(). Very similar, but doesn't require an extra class:

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 */
Run Code Online (Sandbox Code Playgroud)

Or Kotlin:

body.reduce(object : InputStream() {
  override fun read() = -1
}) { s: InputStream, d -> SequenceInputStream(s, d.asInputStream()) }
  .flatMap { inputStream -> /* do something with single InputStream */ }
Run Code Online (Sandbox Code Playgroud)

Benefit of this approach over using collect() is simply you don't need to have a different class to gather things up.

I created a new empty InputStream(), but if that syntax is confusing, you can also replace it with ByteArrayInputStream("".toByteArray()) instead to create an empty ByteArrayInputStream as your initial value instead.

  • 而不是`new InputStream() { public int read() { return -1; } }` 你可以使用 `InputStream.nullInputStream()` (5认同)