1. 概述
在本教程中,我們將演示如何使用 RestTemplate 技術下載大型文件。
2. RestTemplate
RestTemplate 是 Spring 3.0 中引入的阻塞式同步 HTTP 客户端。根據 Spring 文檔,由於 Spring 5.0 引入了 WebClient 作為響應式非阻塞 HTTP 客户端,因此它將在未來被棄用。
3. 常見問題
通常,當我們下載文件時,我們會將其存儲在文件系統中,或將其加載為字節數組。但是,對於大型文件,將數據加載到內存中可能會導致 OutOfMemoryError。因此,我們必須將數據存儲在文件中,並以塊的形式讀取響應。
讓我們首先看一下一些不生效的方法:
首先,如果我們將返回類型設置為 Resource 會發生什麼:
Resource download() {
return new ClassPathResource(locationForLargeFile);
}這段代碼無法正常工作的原因是,ResourceHttpMesssageConverter 會將整個響應體加載到 ByteArrayInputStream 中,仍然會增加我們想要避免的內存壓力。
其次,如果我們返回一個 InputStreamResource 並配置 ResourceHttpMessageConverter#supportsReadStreaming,結果也是不行,因為當我們能夠調用 InputStreamResource.getInputStream() 時,會收到 “socket closed” 錯誤! 這是因為 “execute” 在退出之前關閉了響應輸入流。
所以,我們該如何解決這個問題呢? 實際上,這裏也有兩點:
- 編寫一個自定義的 HttpMessageConverter,支持 File 作為返回值
- 使用 RestTemplate.execute 搭配一個自定義的 ResponseExtractor,將輸入流存儲到 File 中
在本文教程中,我們將使用第二種解決方案,因為它更靈活,而且也需要更少的努力。
4. 不中斷下載
讓我們實現一個 ResponseExtractor,將內容寫入臨時文件 :。
File file = restTemplate.execute(FILE_URL, HttpMethod.GET, null, clientHttpResponse -> {
File ret = File.createTempFile("download", "tmp");
StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(ret));
return ret;
});
Assert.assertNotNull(file);
Assertions
.assertThat(file.length())
.isEqualTo(contentLength);我們這裏使用了StreamUtils.copy來複制響應輸入流到FileOutputStream中,但其他技術和庫也是可用的。
5. 使用暫停和恢復下載
由於我們將下載一個大型文件,因此在某些原因暫停後繼續下載是合理的考慮。
首先,讓我們檢查下載 URL 是否支持恢復下載:
HttpHeaders headers = restTemplate.headForHeaders(FILE_URL);
Assertions
.assertThat(headers.get("Accept-Ranges"))
.contains("bytes");
Assertions
.assertThat(headers.getContentLength())
.isGreaterThan(0);然後我們可以實現一個 RequestCallback,以設置“Range”請求頭並恢復下載:
restTemplate.execute(
FILE_URL,
HttpMethod.GET,
clientHttpRequest -> clientHttpRequest.getHeaders().set(
"Range",
String.format("bytes=%d-%d", file.length(), contentLength)),
clientHttpResponse -> {
StreamUtils.copy(clientHttpResponse.getBody(), new FileOutputStream(file, true));
return file;
});
Assertions
.assertThat(file.length())
.isLessThanOrEqualTo(contentLength);如果我們不知道確切的內容長度,可以使用 Range 標頭的值,通過 String.format 進行設置:
String.format("bytes=%d-", file.length())6. 結論
我們討論了在下載大型文件時可能出現的各種問題。我們還通過使用 RestTemplate 提出瞭解決方案。最後,我們展示瞭如何實現可中斷下載。