使用 WebClient 將大字節 [] 流式傳輸到文件
一、簡介
在本快速教程中,我們將使用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[]
**中。之後,我們將這些字節寫入我們的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 和將其保存在本地的destination
。為了避免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.通過內存調整運行我們的客戶端
隨後,我們會將我們的項目打包為/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);
}
就是這樣。這種方法更通用,因為我們不依賴於文件或內存大小。讓我們將最大內存設置為文件大小的四分之一,並使用之前的相同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 上獲得。