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

后端 未结 4 928
天命终不由人
天命终不由人 2021-02-02 17:11

It seems like it the Spring RestTemplate isn\'t able to stream a response directly to file without buffering it all in memory. What is the proper to achieve this us

相关标签:
4条回答
  • 2021-02-02 17:17

    Store the body to a temporary file and consume

    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);
                    }
                }
        );
    }
    
    0 讨论(0)
  • 2021-02-02 17:22

    I'm not sure if you have access to RestTemplate in your current usage of spring, but this one have worked for me.

    
    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);
    
    
    0 讨论(0)
  • 2021-02-02 17:31

    With recent stable Spring WebFlux (5.2.4.RELEASE as of writing):

    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.

    0 讨论(0)
  • 2021-02-02 17:40

    I cannot test whether or not the following code effectively does not buffer the contents of webClient payload in memory. 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();
    }
    
    0 讨论(0)
提交回复
热议问题