You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 21:15:19