简体   繁体   English

Spring WebClient:如何将大字节 [] 流式传输到文件?

[英]Spring WebClient: How to stream large byte[] to file?

It seems like it the Spring RestTemplate isn't able to stream a response directly to file without buffering it all in memory.似乎 Spring RestTemplate无法将响应直接流式传输到文件而不将其全部缓冲在内存中。 What is the proper to achieve this using the newer Spring 5 WebClient ?使用较新的 Spring 5 WebClient实现这一目标的正确方法是什么?

WebClient client = WebClient.create("https://example.com");
client.get().uri(".../{name}", name).accept(MediaType.APPLICATION_OCTET_STREAM)
                    ....?

I see people have found a few workarounds/hacks to this issue with RestTemplate , but I am more interested in doing it the proper way with the WebClient .我看到人们使用RestTemplate找到了一些解决此问题的解决方法/ RestTemplate ,但我更感兴趣的是使用WebClient以正确的方式进行处理。

There are many examples of using RestTemplate to download binary data but almost all of them load the byte[] into memory.有很多使用RestTemplate下载二进制数据的示例,但几乎所有示例都将byte[]加载到内存中。

With recent stable Spring WebFlux (5.2.4.RELEASE as of writing):使用最近稳定的 Spring WebFlux(撰写本文时为 5.2.4.RELEASE):

final WebClient client = WebClient.create("https://example.com");
final Flux<DataBuffer> dataBufferFlux = client.get()
        .accept(MediaType.TEXT_HTML)
        .retrieve()
        .bodyToFlux(DataBuffer.class); // the magic happens here

final Path path = FileSystems.getDefault().getPath("target/example.html");
DataBufferUtils
        .write(dataBufferFlux, path, CREATE_NEW)
        .block(); // only block here if the rest of your code is synchronous

For me the non-obvious part was the bodyToFlux(DataBuffer.class) , as it is currently mentioned within a generic section about streaming of Spring's documentation, there is no direct reference to it in the WebClient section.对我来说,不明显的部分是bodyToFlux(DataBuffer.class) ,因为它目前在关于Spring 文档通用部分中提到,在 WebClient 部分中没有直接引用它。

I cannot test whether or not the following code effectively does not buffer the contents of webClient payload in memory.我无法测试以下代码是否有效地没有缓冲内存中webClient有效负载的内容。 Nevertheless, i think you should start from there:尽管如此,我认为你应该从那里开始:

public Mono<Void> testWebClientStreaming() throws IOException {
    Flux<DataBuffer> stream = 
            webClient
                    .get().accept(MediaType.APPLICATION_OCTET_STREAM)
                    .retrieve()
            .bodyToFlux(DataBuffer.class);
    Path filePath = Paths.get("filename");
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(filePath, WRITE);
    return DataBufferUtils.write(stream, asynchronousFileChannel)
            .doOnNext(DataBufferUtils.releaseConsumer())
            .doAfterTerminate(() -> {
                try {
                    asynchronousFileChannel.close();
                } catch (IOException ignored) { }
            }).then();
}

Store the body to a temporary file and consume将 body 存储到临时文件并消费

static <R> Mono<R> writeBodyToTempFileAndApply(
        final WebClient.ResponseSpec spec,
        final Function<? super Path, ? extends R> function) {
    return using(
            () -> createTempFile(null, null),
            t -> write(spec.bodyToFlux(DataBuffer.class), t)
                    .thenReturn(function.apply(t)),
            t -> {
                try {
                    deleteIfExists(t);
                } catch (final IOException ioe) {
                    throw new RuntimeException(ioe);
                }
            }
    );
}

Pipe the body and consume管道身体并消耗

static <R> Mono<R> pipeBodyAndApply(
        final WebClient.ResponseSpec spec, final ExecutorService executor,
        final Function<? super ReadableByteChannel, ? extends R> function) {
    return using(
            Pipe::open,
            p -> {
                final Future<Disposable> future = executor.submit(
                        () -> write(spec.bodyToFlux(DataBuffer.class), p.sink())
                                .log()
                                .doFinally(s -> {
                                    try {
                                        p.sink().close();
                                        log.debug("p.sink closed");
                                    } catch (final IOException ioe) {
                                        throw new RuntimeException(ioe);
                                    }
                                })
                                .subscribe(DataBufferUtils.releaseConsumer())
                );
                return just(function.apply(p.source()))
                        .log()
                        .doFinally(s -> {
                            try {
                                final Disposable disposable = future.get();
                                assert disposable.isDisposed();
                            } catch (InterruptedException | ExecutionException e) {
                                e.printStackTrace();
                            }
                        });
            },
            p -> {
                try {
                    p.source().close();
                    log.debug("p.source closed");
                } catch (final IOException ioe) {
                    throw new RuntimeException(ioe);
                }
            }
    );
}

I'm not sure if you have access to RestTemplate in your current usage of spring, but this one have worked for me.我不确定您在当前使用 spring 时是否可以访问RestTemplate ,但是这个对我RestTemplate


RestTemplate restTemplate // = ...;

RequestCallback requestCallback = request -> request.getHeaders()
        .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));

// Streams the response
ResponseExtractor<Void> responseExtractor = response -> {
    // Here I write the response to a file but do what you like
    Path path = Paths.get("http://some/path");
    Files.copy(response.getBody(), path);
    return null;
};
restTemplate.execute(URI.create("www.something.com"), HttpMethod.GET, requestCallback, responseExtractor);

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM