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
相关产品推荐
相关产品推荐

