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 下载大文件时可能遇到的问题及解决方案,包括:

  • ❌ 不推荐的两种方式:返回 ResourceInputStreamResource
  • ✅ 推荐方式:使用 RestTemplate.execute() + ResponseExtractor 直接写入文件
  • ✅ 支持断点续传的实现方式

虽然 RestTemplate 即将被弃用,但在旧项目中仍有使用场景,掌握这些技巧有助于避免内存溢出等常见问题。


原始标题:Download a Large File Through a Spring RestTemplate | Baeldung

» 下一篇: Java Weekly, 第286期