1. 概述
在本文中,我们将介绍几种使用 Spring 的 RestTemplate 下载大文件的方法,并分析常见的“踩坑”点,以及如何绕过这些问题。
2. RestTemplate 简介
RestTemplate 是 Spring 3 引入的一个同步阻塞式 HTTP 客户端。根据 Spring 官方文档,该类在未来版本中将被弃用,取而代之的是 Spring 5 中引入的响应式非阻塞客户端 WebClient。
尽管如此,在某些项目中,尤其是旧版本 Spring 项目中,RestTemplate 仍被广泛使用。
3. 常见踩坑点
通常下载文件时,我们有两种方式处理:
- 将文件保存到磁盘
- 读入内存(如 byte[])
但面对大文件时,直接读入内存很容易导致 OutOfMemoryError
。因此,我们需要在接收响应流的同时写入磁盘,避免一次性加载整个文件。
来看两个“无效方案”:
3.1 返回 Resource 类型 ❌
Resource download() {
return new ClassPathResource(locationForLargeFile);
}
问题在于:ResourceHttpMessageConverter 会将整个响应体加载到 ByteArrayInputStream
中,导致内存压力增大。
3.2 使用 InputStreamResource ❌
InputStreamResource download() {
return new InputStreamResource(clientHttpResponse.getBody());
}
即使配置了 ResourceHttpMessageConverter#supportsReadStreaming
,也无法解决问题,因为 RestTemplate.execute()
方法在返回前会关闭响应流,导致后续调用 getInputStream()
时抛出 “socket closed” 错误。
✅ 正确做法
我们有两种解决方案:
- 自定义
HttpMessageConverter
支持返回类型为File
- 使用
RestTemplate.execute()
配合自定义ResponseExtractor
直接写入文件
本文推荐第二种方式,简单粗暴,实现起来也更灵活。
4. 非断点续传下载
我们可以使用 RestTemplate.execute()
方法配合 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
,当然你也可以使用其他方式,例如 BufferedOutputStream
或第三方库如 Apache Commons IO。
5. 支持断点续传的下载
下载大文件时,支持暂停和恢复是非常实用的功能。要实现断点续传,首先要确认目标服务器是否支持。
5.1 检查服务器是否支持断点续传
HttpHeaders headers = restTemplate.headForHeaders(FILE_URL);
Assertions
.assertThat(headers.get("Accept-Ranges"))
.contains("bytes");
Assertions
.assertThat(headers.getContentLength())
.isGreaterThan(0);
如果返回的 Accept-Ranges
头中包含 bytes
,说明支持以字节范围的方式请求文件片段。
5.2 实现断点续传逻辑
我们可以使用 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);
如果不知道确切的 contentLength
,可以只设置起始位置:
String.format("bytes=%d-", file.length())
这样服务器会从指定位置一直返回到文件结尾。
6. 小结
本文介绍了使用 RestTemplate 下载大文件时可能遇到的问题及解决方案,包括:
- ❌ 不推荐的两种方式:返回
Resource
和InputStreamResource
- ✅ 推荐方式:使用
RestTemplate.execute()
+ResponseExtractor
直接写入文件 - ✅ 支持断点续传的实现方式
虽然 RestTemplate 即将被弃用,但在旧项目中仍有使用场景,掌握这些技巧有助于避免内存溢出等常见问题。