概述
在这个快速教程中,我们将使用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. 使用ExchangeStrategies
处理大文件的WebClient
我们从一个简单但功能有限的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[]
。然后,我们将这些字节写入我们的path
并返回下载的字节数。让我们创建一个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和本地保存的位置。为了在我们的client
中避免DataBufferLimitException
,我们需要配置一个交换策略来限制可加载到内存中的字节数。而不是定义固定大小,我们将使用Runtime
获取应用程序配置的总内存。请注意,这并不推荐,只是为了演示目的:
private static ExchangeStrategies useMaxMemory() {
long totalMemory = Runtime.getRuntime().maxMemory();
return ExchangeStrategies.builder()
.codecs(configurer -> configurer.defaultCodecs()
.maxInMemorySize((int) totalMemory)
)
.build();
}
换句话说,交换策略定制了我们的client
处理请求的方式。在这种情况下,我们使用构建器的codecs()
方法,因此我们没有替换任何默认设置。
3.1. 调整内存后运行我们的客户端
接下来,我们将我们的项目打包成一个jar,存放在/tmp/app.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);
}
就这样。这个方法更具灵活性,因为我们不依赖于文件或内存大小。让我们设置最大内存为文件大小的四分之一,使用之前的endpoint运行它:
client='com.baeldung.streamlargefile.client.LargeFileDownloadWebClient'
java -Xmx32m -cp /tmp/app.jar $client $endpoint /tmp/download.dat
最后,我们将得到成功输出,尽管我们的应用程序的总内存小于文件大小:
downloaded 134217728 bytes
5. 总结
在这篇文章中,我们学习了使用WebClient
下载任意大文件的不同方法。首先,我们了解了如何定义WebClient
操作的可用内存量。然后,我们看到了这种方法的局限性。最重要的是,我们学会了如何使我们的客户端高效地使用内存。
如往常一样,源代码可以在GitHub上找到。