1. 简介
在本快速教程中,我们将使用WebClient从服务器流式传输一个大文件。为了说明,我们将创建一个简单的控制器和两个客户端。最终,我们将了解如何以及何时使用Spring的DataBuffer和DataBufferUtils。
2. 使用简单服务器的场景
我们将从一个用于下载任意文件的简单控制器开始。首先,我们将构造一个FileSystemResource,传递一个文件Path,然后将其作为主体包装到我们的ResponseEntity:
@RestController
@RequestMapping("/large-file")
public class LargeFileController {
@GetMapping
ResponseEntity<Resource> get() {
return ResponseEntity.ok()
.body(new FileSystemResource(Paths.get("/tmp/large.dat")));
}
}
其次,我们需要生成我们正在引用的文件。由于内容对于理解本教程并不重要,因此我们将使用fallocate在磁盘上保留指定的大小而无需写入任何内容。所以,让我们通过运行以下命令来创建我们的大文件:
fallocate -l 128M /tmp/large.dat
最后,我们有一个客户可以下载的文件。所以,我们准备开始写我们的客户端。
3. WebClient与大文件的ExchangeStrategies
我们将从一个简单但有限的WebClient开始下载我们的文件。我们将使用ExchangeStrategies来提高可用于exchange()操作的内存限制。这样,我们可以操作更多的字节,但我们仍然受限于JVM可用的最大内存。让我们使用bodyToMono()从服务器获取Mono<byte[]>:
public class LimitedFileDownloadWebClient {
public static long fetch(WebClient client, String destination) {
Mono<byte[]> mono = client.get()
.retrieve()
.bodyToMono(byte[].class);
byte[] bytes = mono.block();
Path path = Paths.get(destination);
Files.write(path, bytes);
return bytes.length;
}
// ...
}
换句话说,我们将整个响应内容检索到一个byte[]中。之后,我们将这些字节写入我们的路径并返回下载的字节数。让我们创建一个main()方法来测试它:
public static void main(String... args) {
String baseUrl = args[0];
String destination = args[1];
WebClient client = WebClient.builder()
.baseUrl(baseUrl)
.exchangeStrategies(useMaxMemory())
.build();
long bytes = fetch(client, destination);
System.out.printf("downloaded %d bytes", bytes);
}
此外,我们还需要两个参数:下载URL和将其保存在本地的目的地。为了避免客户端出现DataBufferLimitException,让我们配置一个交换策略来限制可加载到内存中的字节数。我们将使用Runtime获取为我们的应用程序配置的总内存,而不是定义固定大小。请注意,不推荐这样做,仅用于演示目的:
private static ExchangeStrategies useMaxMemory() {
long totalMemory = Runtime.getRuntime().maxMemory();
return ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs()
.maxInMemorySize((int) totalMemory)
)
.build();
}
澄清一下,交换策略定制了我们的客户端处理请求的方式。在这种情况下,我们使用构建器中的codecs()方法,因此我们不替换任何默认设置。
3.1 通过内存调整运行我们的客户端
随后,我们将项目打包为/tmp/app.jar中的jar,并在localhost:8081上运行我们的服务器。然后,让我们定义一些变量并从命令行运行我们的客户端:
limitedClient='com.baeldung.streamlargefile.client.LimitedFileDownloadWebClient'
endpoint='http://localhost:8081/large-file'
java -Xmx256m -cp /tmp/app.jar $limitedClient $endpoint /tmp/download.dat
请注意,我们允许我们的应用程序使用两倍于128M文件大小的内存。事实上,我们将下载我们的文件并获得以下输出:
downloaded 134217728 bytes
另一方面,如果我们没有分配足够的内存,我们会得到一个OutOfMemoryError:
$ java -Xmx64m -cp /tmp/app.jar $limitedClient $endpoint /tmp/download.dat
reactor.netty.ReactorNetty$InternalNettyException: java.lang.OutOfMemoryError: Direct buffer memory
这种方法不依赖于Spring Core实用程序。但是,它是有限的,因为我们无法下载任何大小接近应用程序最大内存的文件。
4. 使用DataBuffer的任何文件大小的WebClient
一种更安全的方法是使用DataBuffer和DataBufferUtils以块的形式流式传输我们的下载,这样整个文件就不会加载到内存中。然后,这一次,我们将使用bodyToFlux()来检索Flux<DataBuffer>,将其写入我们的path,并以字节为单位返回其大小:
public class LargeFileDownloadWebClient {
public static long fetch(WebClient client, String destination) {
Flux<DataBuffer> flux = client.get()
.retrieve()
.bodyToFlux(DataBuffer.class);
Path path = Paths.get(destination);
DataBufferUtils.write(flux, path)
.block();
return Files.size(path);
}
// ...
}
最后,让我们编写main方法来接收我们的参数,创建一个WebClient并获取我们的文件:
public static void main(String... args) {
String baseUrl = args[0];
String destination = args[1];
WebClient client = WebClient.create(baseUrl);
long bytes = fetch(client, destination);
System.out.printf("downloaded %d bytes", bytes);
}
就是这样。这种方法更通用,因为我们不依赖于文件或内存大小。让我们将最大内存设置为文件大小的四分之一,并使用之前的相同端点运行它:
client='cn.tuyucheng.taketoday.streamlargefile.client.LargeFileDownloadWebClient'
java -Xmx32m -cp /tmp/app.jar $client $endpoint /tmp/download.dat
最后,我们将获得成功的输出,即使我们的应用程序的总内存小于文件的大小:
downloaded 134217728 bytes
5. 总结
在本文中,我们了解了使用WebClient下载任意大文件的不同方法。首先,我们了解了如何定义可用于WebClient操作的内存量。然后,我们看到了这种方法的缺点。最重要的是,我们学会了如何让我们的客户端有效地使用内存。
与往常一样,本教程的完整源代码可在GitHub上获得。