Spring Webflux零拷贝返回文件后如何非阻塞删除文件
Spring WebFlux 零拷贝返回临时文件+传输完成后自动删除实现方案
核心前提
Spring WebFlux 基于Netty运行时,返回FileSystemResource会自动触发操作系统级sendfile零拷贝逻辑,无需额外做传输层改造,这套机制本身就满足高吞吐、低CPU消耗的要求,之前的方案失效主要是三个原因:
- 重写
getInputStream()在close()时删除的逻辑,仅适用于Servlet阻塞场景,Netty零拷贝传输完全不会走InputStream读取路径,回调根本不会触发 WebFilter的执行时机是处理链返回响应对象的节点,此时响应内容还未真正写入到客户端,提前删除文件会导致客户端拿到损坏的文件- 直接调用
Files.delete()属于阻塞IO操作,在Netty EventLoop线程执行会违反响应式编程规范,影响整体服务吞吐量
具体实现
1. 自定义支持清理回调的资源类
利用Spring Resource自带的cleanup()回调机制——这个方法会在响应内容完全写入网卡缓冲区(包括正常传输完成、连接异常中断两种场景)后被框架自动调用,时机完全准确:
import org.springframework.core.io.FileSystemResource; import java.io.File; import java.io.IOException; public class AutoDeleteFileSystemResource extends FileSystemResource { private final Runnable onTransferComplete; public AutoDeleteFileSystemResource(File file, Runnable onTransferComplete) { super(file); this.onTransferComplete = onTransferComplete; } @Override public void cleanup() throws IOException { super.cleanup(); // 传输完成/中断后触发清理逻辑 onTransferComplete.run(); } }
2. 非阻塞执行文件删除
将阻塞的文件删除操作调度到boundedElastic线程池执行,完全不占用Netty EventLoop线程资源,符合WebFlux非阻塞规范,接口层示例代码:
import org.springframework.core.io.FileSystemResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import java.io.File; import java.io.IOException; import java.nio.file.Files; @RestController public class FileDownloadController { @GetMapping("/download/temp/{fileId}") public Mono<ResponseEntity<FileSystemResource>> downloadTempFile(@PathVariable String fileId) { // 业务逻辑获取待返回的临时文件 File tempFile = loadTempFileById(fileId); return Mono.just(ResponseEntity.ok() .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + tempFile.getName() + "\"") .body(new AutoDeleteFileSystemResource(tempFile, () -> // 阻塞删除操作调度到专用阻塞线程池执行,不阻塞EventLoop Mono.fromRunnable(() -> { try { Files.deleteIfExists(tempFile.toPath()); } catch (IOException ignored) { // 可按需添加删除失败的日志记录 } }).subscribeOn(Schedulers.boundedElastic).subscribe() ))); } private File loadTempFileById(String fileId) { // 替换为实际的临时文件查询逻辑 return new File("/tmp/export/" + fileId + ".tmp"); } }
注意事项
- 零拷贝生效验证:确保服务运行在Netty环境(不要部署为Servlet WAR包),待传输文件大小大于Netty默认的零拷贝阈值1KB即可自动生效,传输过程无用户态内存拷贝,CPU消耗极低
- 兜底机制:建议配合定时任务定期清理超过最大存活时长的临时文件,覆盖服务宕机、进程异常退出等极端场景下的文件残留问题
- 阈值调整:如果需要调整零拷贝触发的最小文件大小,可自定义
ServerCodecConfigurer配置项修改对应参数
内容的提问来源于stack exchange,提问作者Lakatos Gyula
相关产品推荐
相关产品推荐

