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