概述

在这个快速教程中,我们将使用WebClient从服务器流式传输大文件。我们将创建一个简单的控制器和两个客户端,最终了解如何以及何时使用Spring的DataBufferDataBufferUtils

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

一个更安全的方法是使用DataBufferDataBufferUtils来分块流式下载,以防止整个文件加载到内存中。这次,我们将使用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上找到。