Spring WebClient: Как передать большой байт [] в файл?

Вроде как весна RestTemplate не может передать ответ непосредственно в файл без буферизации всего в памяти. Как правильно добиться этого, используя более новую версию Spring 5? WebClient?

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

Я вижу, что люди нашли несколько способов решения этой проблемы с RestTemplate, но я больше заинтересован в том, чтобы сделать это правильно с WebClient,

Есть много примеров использования RestTemplate загружать двоичные данные, но почти все они загружают byte[] в память.

2 ответа

С недавним стабильным 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

Для меня неочевидной частью была bodyToFlux(DataBuffer.class), поскольку это в настоящее время упоминается в общем разделе о потоковой передаче документации Spring, в разделе WebClient нет прямой ссылки на него.

Я не могу проверить, эффективно ли следующий код не буферизует содержимое webClient полезная нагрузка в памяти. Тем не менее, я думаю, что вы должны начать с этого:

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();
}

Сохраните тело во временном файле и используйте

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);
                }
            }
    );
}

Трубить тело и потреблять

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);
                }
            }
    );
}

Это сработало для меня, без использования WebClient, но с использованием HttpURLConnection вместо этого. В моем случае у меня есть огромные XML-файлы, которые мне нужно получить из другого API и вернуть их.

@GetMapping(value = "/hugeXml/{fileName}", produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<String> getHugeXml(@PathVariable String fileName) {
        try {
            URL myURL = new URL("http://some-other-url.com/xml_files/" + fileName + ".xml");
            HttpURLConnection myURLConnection = (HttpURLConnection) myURL.openConnection();
            BufferedReader reader = new BufferedReader(new InputStreamReader(myURLConnection.getInputStream()));
            String result = reader.lines().collect(Collectors.joining());
            return ResponseEntity.ok(result);
        } catch (IOException e) {
            String msg = "IO exception occured! Message = " + e.getMessage();
            System.out.println(msg);
            e.printStackTrace();
            return ResponseEntity.badRequest().body(msg);
        }
    }

Я не уверен, что у вас есть доступ к 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);

Другие вопросы по тегам