1. 簡介
本快速教程將使用 <em >WebClient</em> 從服務器端流式傳輸一個大型文件。 為了説明,我們將創建一個簡單的控制器和兩個客户端。 最終,我們將學習如何以及何時使用 Spring 的 <em >DataBuffer</em> 和 `DataBufferUtils》。
2. 使用一個簡單的服務器的場景
我們將從一個用於下載任意文件的簡單控制器開始。 首先,我們將構造一個 FileSystemResource,傳遞一個 Path,然後將其包裝為 ResponseEntity 的 body:
@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 和一個本地保存的 目標路徑。為了避免在我們的 客户端中出現 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. 使用內存調整運行我們的客户端
隨後,我們將項目打包為 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. 使用 處理任意文件大小,藉助
採用更安全的做法是使用 和 ,通過分塊流式傳輸下載內容,避免整個文件加載到內存中。隨後,我們將使用 來獲取一個 , 將其寫入到我們的 ,並返回其字節大小:
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);
}
// ...
}最後,我們來編寫主方法以接收我們的參數,創建 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);
}<p>這就是全部。 <strong >這種方法更具靈活性,因為我們不依賴文件或內存大小。</strong> 讓我們將最大內存設置為文件大小的四分之一,並使用前面提到的相同的 <em >端點</em> 運行它:</p>
client='com.baeldung.streamlargefile.client.LargeFileDownloadWebClient'
java -Xmx32m -cp /tmp/app.jar $client $endpoint /tmp/download.dat最終,即使我們的應用程序的總內存小於文件的大小,我們也能獲得成功的輸出:
downloaded 134217728 bytes5. 結論
在本文中,我們學習瞭如何使用 <em >WebClient</em> 下載任意大小的文件。首先,我們學習瞭如何定義用於我們的 <em >WebClient</em> 操作的可用內存。然後,我們看到了這種方法的缺點。 最重要的是,我們學習瞭如何使客户端高效地使用內存。