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

Spring Boot异步MultipartFile上传存储临时文件丢失问题求助

问题分析

你遇到的NoSuchFileException本质是Spring在请求处理完成后会自动清理MultipartFile对应的临时文件,而@Async标注的方法是在请求线程结束后才异步执行的,这时候临时文件已经被删除,调用transferTo自然会找不到文件。

解决方案

下面提供几种可靠的解决思路,兼顾大文件上传场景(避免OOM):

方案1:预存到自定义临时目录(推荐)

在请求线程中先将MultipartFile转存到自己管理的临时位置,再把临时文件路径传给异步方法,异步处理完成后清理临时文件。这样完全脱离Spring的临时文件生命周期控制,不会出现文件被提前删除的问题。

修改主方法代码:

public ResponseEntity<?> save(MultipartFile[] files) {
    if (files.length == 0) {
        return ResponseEntity.badRequest().body("No files selected");
    }

    DigikyblikFileStorage storage = digikyblikFileStoragesService.getIdealStorage();
    UUID bucketUUID = UUID.randomUUID();
    List<Path> tempFilePaths = new ArrayList<>();

    try {
        // 先将文件转存到自定义临时目录
        for (MultipartFile file : files) {
            Path tempFile = Files.createTempFile("upload_temp_", ".tmp");
            file.transferTo(tempFile);
            tempFilePaths.add(tempFile);
        }

        // 异步处理临时文件
        asyncService.saveFiles(tempFilePaths, storage, bucketUUID, files);

        // 保存记录逻辑不变
        List<File> filesData = new ArrayList<>();
        for (MultipartFile file : files) {
            filesData.add(new File(file.getOriginalFilename(), file.getSize()));
        }
        BucketFilesRecord record = new BucketFilesRecord();
        record.setBucketUUID(bucketUUID);
        record.setStorageId(storage.getId());
        record.setFilesInfo(new ObjectMapper().writeValueAsString(filesData));
        bucketFilesRecordRepository.save(record);

    } catch (JsonProcessingException e) {
        logger.error("Error when setting files info for bucketUUID: " + bucketUUID, e);
        return ResponseEntity.internalServerError().body("Error when setting files info for bucketUUID: " + bucketUUID);
    } catch (IOException e) {
        logger.error("Error when handling temp files for bucketUUID: " + bucketUUID, e);
        // 清理已创建的临时文件
        tempFilePaths.forEach(path -> {
            try {
                Files.deleteIfExists(path);
            } catch (IOException ex) {
                logger.error("Failed to delete temp file: " + path, ex);
            }
        });
        return ResponseEntity.internalServerError().body("Error when preparing files for upload");
    }

    return new ResponseEntity<>(new FilesUploadedResponse(bucketUUID), HttpStatus.OK);
}

修改异步方法代码:

@Async
public void saveFiles(List<Path> tempFilePaths, DigikyblikFileStorage storage, UUID bucketUUID, MultipartFile[] originalFiles) {
    logger.info("Start storing files for bucketUUID: " + bucketUUID);
    Path targetDir = Paths.get(storage.getStoragePath(), bucketUUID.toString());

    try {
        Files.createDirectories(targetDir);
        // 将临时文件移动到目标目录
        for (int i = 0; i < tempFilePaths.size(); i++) {
            Path tempPath = tempFilePaths.get(i);
            MultipartFile originalFile = originalFiles[i];
            Path targetPath = targetDir.resolve(originalFile.getOriginalFilename());
            // 移动文件(比复制后删除更高效)
            Files.move(tempPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
        }
    } catch (IOException e) {
        logger.error("Couldn't store files for bucketUUID: " + bucketUUID, e);
    } finally {
        // 无论成功失败,都清理临时文件
        tempFilePaths.forEach(path -> {
            try {
                Files.deleteIfExists(path);
            } catch (IOException ex) {
                logger.error("Failed to delete temp file: " + path, ex);
            }
        });
    }
}

方案2:配置Spring Multipart属性,手动管理临时文件

通过配置让Spring不自动清理Multipart临时文件,同时指定自定义临时目录,再通过定时任务清理过期文件。这种方式无需修改太多业务代码,但需要额外维护临时文件的清理逻辑。

配置application.properties:

# 指定自定义临时目录,避免和系统临时目录冲突
spring.servlet.multipart.location=/your/custom/tmp/dir
# 禁止Spring自动清理临时文件
spring.servlet.multipart.cleanup=false
# 设置文件阈值(超过该大小的文件会存到磁盘,这里设为0表示所有文件都存磁盘)
spring.servlet.multipart.file-size-threshold=0

添加定时清理任务:

@Scheduled(cron = "0 0 0 * * ?") // 每天凌晨执行
public void cleanupExpiredTempFiles() {
    Path tempDir = Paths.get("/your/custom/tmp/dir");
    if (!Files.exists(tempDir)) {
        return;
    }
    try (DirectoryStream<Path> stream = Files.newDirectoryStream(tempDir)) {
        for (Path file : stream) {
            FileTime lastModified = Files.getLastModifiedTime(file);
            // 删除24小时前的临时文件
            if (System.currentTimeMillis() - lastModified.toMillis() > 24 * 60 * 60 * 1000) {
                Files.deleteIfExists(file);
            }
        }
    } catch (IOException e) {
        logger.error("Failed to cleanup temp files", e);
    }
}

注意事项

  • 方案1的优势是完全可控,不会出现磁盘被临时文件占满的风险(因为处理完就删除),适合大文件上传场景。
  • 方案2需要确保定时任务正常运行,否则临时目录会持续占用磁盘空间。
  • 无论哪种方案,都要做好异常处理,避免文件遗漏或磁盘浪费。

内容的提问来源于stack exchange,提问作者Sizor

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 06:30:48