如何在Spring Controller中内存高效地返回AWS S3大文件?
解决方案
核心思路是避免将完整S3文件加载到JVM内存,共有两种可行的落地方式:
方案1:后端中转输入流(适用用户无法直接访问S3的网络隔离场景)
直接将S3返回的输入流透传给前端,仅缓冲少量流式数据,不会占用大量内存。
第一步:调整S3服务层逻辑,返回文件元数据与输入流
以AWS SDK v2为例:
import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.model.HeadObjectRequest; import software.amazon.awssdk.services.s3.model.S3Object; // AWSS3Service新增方法 public long getFileSize(String keyName) { HeadObjectRequest headRequest = HeadObjectRequest.builder() .bucket("你的S3桶名") .key(keyName) .build(); return s3Client.headObject(headRequest).contentLength(); } public S3Object getFileInputStream(String keyName) { GetObjectRequest getRequest = GetObjectRequest.builder() .bucket("你的S3桶名") .key(keyName) .build(); return s3Client.getObject(getRequest); }
第二步:改造Controller返回InputStreamResource
Spring MVC会自动在请求结束后关闭输入流,无需手动处理资源释放:
import org.springframework.core.io.InputStreamResource; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @RestController @RequestMapping(value= "/s3") public class AWSS3Ctrl { @Autowired private AWSS3Service service; @GetMapping(value= "/download") public ResponseEntity<InputStreamResource> downloadFile(@RequestParam(value= "fileName") final String keyName) { // 先校验文件存在性,获取文件长度 long contentLength = service.getFileSize(keyName); // 获取S3流式对象 S3Object s3File = service.getFileInputStream(keyName); InputStreamResource resource = new InputStreamResource(s3File); // 编码文件名解决中文乱码问题 String encodedFileName = URLEncoder.encode(keyName, StandardCharsets.UTF_8).replace("+", "%20"); return ResponseEntity .ok() .contentLength(contentLength) .contentType(MediaType.APPLICATION_OCTET_STREAM) .header("Content-disposition", "attachment; filename*=UTF-8''" + encodedFileName) .body(resource); } }
方案2:S3预签名URL重定向(推荐方案,无服务器带宽占用、性能最优)
利用S3预签名URL能力,生成有时效性的私有文件下载链接,后端直接重定向到该地址即可,完全不需要中转文件流。
第一步:S3服务层新增预签名生成逻辑
import software.amazon.awssdk.services.s3.model.GetObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import java.time.Duration; // AWSS3Service新增方法 public String generatePresignedDownloadUrl(String keyName, Duration expireDuration) { GetObjectRequest getRequest = GetObjectRequest.builder() .bucket("你的S3桶名") .key(keyName) // 强制指定下载文件名,可选配置 .responseContentDisposition("attachment; filename=\"" + keyName + "\"") .build(); GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder() .signatureDuration(expireDuration) .getObjectRequest(getRequest) .build(); return s3Presigner.presignGetObject(presignRequest).url().toString(); }
第二步:Controller改造为重定向逻辑
@GetMapping(value= "/download") public ResponseEntity<Void> downloadFile(@RequestParam(value= "fileName") final String keyName) { // 生成5分钟有效期的下载链接,可根据业务调整过期时间 String presignedUrl = service.generatePresignedDownloadUrl(keyName, Duration.ofMinutes(5)); return ResponseEntity .status(HttpStatus.FOUND) .header("Location", presignedUrl) .build(); }
方案选型建议
- 若业务需要做下载权限校验、下载次数统计等逻辑,两种方案都可以在生成链接/返回流之前做校验,不影响功能
- 若用户侧网络可以直接访问S3公网/内网地址,优先选预签名方案,无需占用后端服务器带宽,大文件下载性能提升明显
- 若网络隔离用户无法直接访问S3,选择流式中转方案即可,完全避免OOM问题
内容的提问来源于stack exchange,提问作者NineWoranop
相关产品推荐
相关产品推荐

