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

Rest服务实现文件下载完成/中断后自动删除的解决方案

解决文件下载完成/中断后自动删除的问题

直接在创建Resource后删除文件会报错,因为Spring返回ResponseEntity后才会开始向客户端传输文件内容,此时文件已被删除就会导致读取失败。必须等文件流完全传输完毕(或传输中断)后再执行删除操作,以下是两种可行方案:

方案一:自定义Resource包装类,流关闭时触发删除

通过包装原始UrlResource,重写getInputStream方法返回一个装饰过的输入流,在流关闭时自动删除文件。

自定义AutoDeleteResource类

import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class AutoDeleteResource implements Resource {

    private final Resource delegate;
    private final Path filePath;

    public AutoDeleteResource(Path filePath) throws IOException {
        this.delegate = new UrlResource(filePath.toUri());
        this.filePath = filePath;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        InputStream originalStream = delegate.getInputStream();
        // 包装输入流,关闭时执行删除
        return new InputStream() {
            @Override
            public int read() throws IOException {
                return originalStream.read();
            }

            @Override
            public void close() throws IOException {
                try {
                    originalStream.close();
                } finally {
                    // 无论流关闭是否成功,都尝试删除文件
                    try {
                        Files.deleteIfExists(filePath);
                    } catch (IOException e) {
                        logger.error("删除文件失败: " + filePath, e);
                    }
                }
            }

            // 重写其他read方法提升性能
            @Override
            public int read(byte[] b) throws IOException {
                return originalStream.read(b);
            }

            @Override
            public int read(byte[] b, int off, int len) throws IOException {
                return originalStream.read(b, off, len);
            }
        };
    }

    // 以下方法直接委托给原始Resource实现
    @Override
    public boolean exists() {
        return delegate.exists();
    }

    @Override
    public boolean isReadable() {
        return delegate.isReadable();
    }

    @Override
    public Resource createRelative(String relativePath) throws IOException {
        return delegate.createRelative(relativePath);
    }

    @Override
    public String getFilename() {
        return delegate.getFilename();
    }

    @Override
    public String getDescription() {
        return delegate.getDescription();
    }

    @Override
    public boolean isOpen() {
        return delegate.isOpen();
    }

    @Override
    public java.net.URL getURL() throws IOException {
        return delegate.getURL();
    }

    @Override
    public java.net.URI getURI() throws IOException {
        return delegate.getURI();
    }

    @Override
    public java.io.File getFile() throws IOException {
        return delegate.getFile();
    }

    @Override
    public long contentLength() throws IOException {
        return delegate.contentLength();
    }

    @Override
    public long lastModified() throws IOException {
        return delegate.lastModified();
    }
}

修改原下载接口

@GetMapping("/download/{fileName:.+}")
public ResponseEntity<Resource> downloadFile(@PathVariable String fileName) {
    try {
        Path filePath = Paths.get(directoryPath).resolve(fileName);
        AutoDeleteResource resource = new AutoDeleteResource(filePath);
        if (resource.exists() && resource.isReadable()) {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
            headers.setContentDispositionFormData("attachment", fileName);
            return ResponseEntity.ok().headers(headers).body(resource);
        } else {
            logger.error("文件不存在: " + fileName );
            return ResponseEntity.notFound().build();
        }
    } catch (Exception e) {
        logger.error("下载文件出错: " + fileName, e);
        return ResponseEntity.notFound().build();
    }
}

方案二:用StreamingResponseBody手动控制流传输

直接通过StreamingResponseBody手动处理文件流的读写,在传输完成(或异常中断)后执行删除,代码更简洁。

@GetMapping("/download/{fileName:.+}")
public ResponseEntity<StreamingResponseBody> downloadFile(@PathVariable String fileName) {
    try {
        Path filePath = Paths.get(directoryPath).resolve(fileName);
        if (!Files.exists(filePath) || !Files.isReadable(filePath)) {
            logger.error("文件不存在: " + fileName );
            return ResponseEntity.notFound().build();
        }

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        headers.setContentDispositionFormData("attachment", fileName);

        StreamingResponseBody responseBody = outputStream -> {
            try (InputStream inputStream = Files.newInputStream(filePath)) {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = inputStream.read(buffer)) != -1) {
                    outputStream.write(buffer, 0, bytesRead);
                }
                outputStream.flush();
            } finally {
                // 流关闭后执行删除
                try {
                    Files.deleteIfExists(filePath);
                } catch (IOException e) {
                    logger.error("删除文件失败: " + fileName, e);
                }
            }
        };

        return ResponseEntity.ok().headers(headers).body(responseBody);
    } catch (Exception e) {
        logger.error("下载文件出错: " + fileName, e);
        return ResponseEntity.notFound().build();
    }
}

方案对比

  • 方案一基于Spring的Resource抽象,适配Spring生态,适合需要复用Resource逻辑的场景。
  • 方案二更直接,无需实现大量接口方法,代码量更少,适合简单的下载删除场景。

内容的提问来源于stack exchange,提问作者Владимир Гичев

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 03:12:03