Spring Boot文件上传:解决Failed to perform cleanup of multipart items错误

15 次阅读

Spring Boot项目处理文件上传时,如果日志中出现Failed to perform cleanup of multipart items,通常意味着文件上传请求已经进入了Multipart处理流程,但Spring在请求结束后清理临时文件或Multipart资源时发生了异常。这个问题经常出现在大文件上传、临时目录权限异常、磁盘空间不足、文件句柄未释放以及Multipart配置不合理等场景中。

真正排查这个错误时,不能只盯着Failed to perform cleanup这一行日志。它更多是一个“清理失败”的结果,具体原因通常隐藏在后面的Caused by异常中。

一、Failed to perform cleanup of multipart items是什么错误

Spring Boot接收文件上传请求时,一般会通过Multipart机制处理multipart/form-data请求。

例如:

Java
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    return file.getOriginalFilename();
}

浏览器上传文件后,Spring会负责:

  1. 接收HTTP请求;

  2. 解析Multipart请求;

  3. 保存上传内容;

  4. 将文件包装成MultipartFile;

  5. 调用Controller处理业务;

  6. 请求结束后清理Multipart临时资源。

如果最后一步清理失败,就可能出现类似日志:

Failed to perform cleanup of multipart items

常见完整异常形式可能类似:

org.springframework.web.multipart.MultipartException:
Failed to perform cleanup of multipart items

Caused by: java.io.IOException: ...

需要注意,这个异常并不一定代表“文件上传失败”。

有些情况下,文件已经成功上传并完成业务处理,只是在请求结束时删除临时文件失败。因此,判断问题时应该结合异常发生的位置以及Caused by信息进行分析。

二、为什么会出现Multipart清理失败

Spring Boot文件上传过程中,如果上传内容超过一定大小,或者Multipart解析器采用临时文件存储方式,服务器通常会产生临时文件。

请求结束后,这些临时文件应该被删除。

例如:

上传文件
   ↓
Multipart解析
   ↓
临时目录创建文件
   ↓
Controller处理文件
   ↓
请求结束
   ↓
删除临时文件

如果最后删除文件失败,就可能出现:

Failed to perform cleanup of multipart items

比较常见的原因主要有以下几类。

1. 临时目录权限不足

Linux服务器上的临时目录通常涉及:

/tmp

或者应用服务器配置的其他临时目录。

如果运行Spring Boot的用户没有读写或删除权限,就可能导致Multipart文件无法正常清理。

可以查看目录权限:

Bash
ls -ld /tmp

如果Spring Boot运行在独立用户下,还应该确认该用户对Multipart临时目录具有正常的访问权限。

如果应用配置了自定义临时目录,例如:

properties
spring.servlet.multipart.location=/data/upload/tmp

则需要重点检查:

Bash
ls -ld /data/upload/tmp

以及目录所属用户和权限。

2. 磁盘空间不足

文件上传尤其容易受到磁盘空间影响。

检查磁盘:

Bash
df -h

同时检查inode:

Bash
df -i

有时候磁盘容量还有剩余,但是inode已经耗尽,同样无法创建或删除文件。

如果服务器运行了大量上传任务,还需要关注临时目录是否积累了大量文件。

可以查看:

Bash
du -sh /tmp

如果使用自定义Multipart目录:

Bash
du -sh /data/upload/tmp

磁盘空间不足时,优先清理无用临时文件,同时检查是否存在异常上传任务持续产生大量文件。

三、检查真正的Caused by异常

这是排查该问题最重要的一步。

例如日志可能出现:

Failed to perform cleanup of multipart items

java.io.IOException: Cannot delete ...

或者:

Caused by: java.nio.file.AccessDeniedException

也可能是:

Caused by: java.io.FileNotFoundException

不同的Caused by代表完全不同的问题。

因此,不建议看到:

Failed to perform cleanup of multipart items

就直接修改Spring Boot配置。

应该从日志中向下寻找:

Caused by:

重点关注异常类型、文件路径以及具体操作。

如果是:

AccessDeniedException

重点检查权限。

如果是:

No space left on device

重点检查磁盘。

如果涉及:

Too many open files

则应该检查文件句柄数量以及代码中的流关闭问题。

四、检查Spring Boot文件上传配置

Spring Boot默认提供了Multipart文件上传支持,可以通过配置文件调整上传大小等参数。

例如:

properties
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=110MB

其中:

max-file-size

表示单个文件允许的最大大小。

max-request-size

表示整个Multipart请求允许的最大大小。

如果上传多个文件:

file1 = 50MB
file2 = 50MB
file3 = 50MB

那么请求总大小可能明显超过单个文件限制,因此不能只配置max-file-size。

YAML写法如下:

YAML
spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 110MB

需要注意,上传大小配置主要用于解决文件大小限制问题。如果日志明确指出的是临时文件删除失败,那么单纯增大max-file-size通常无法解决清理异常。

五、检查Multipart临时文件目录

如果项目对上传目录进行了自定义配置,需要特别检查目录是否真实存在。

例如:

properties
spring.servlet.multipart.location=/data/upload/tmp

确认目录:

Bash
ls -ld /data/upload/tmp

如果不存在,可以创建:

Bash
mkdir -p /data/upload/tmp

然后根据应用运行用户设置合适的权限。

例如应用由app用户运行:

Bash
chown -R app:app /data/upload/tmp

权限设置不建议直接使用:

Bash
chmod -R 777 /data/upload/tmp

虽然这种方式可能快速验证权限问题,但在生产环境中会扩大目录访问权限,存在安全风险。

更合理的方式是让实际运行Spring Boot应用的用户拥有必要的读、写、删除权限。

六、检查代码是否提前删除Multipart文件

一个比较容易忽略的问题,是业务代码自己处理了Multipart临时文件。

例如:

Java
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) throws Exception {

    Path path = Paths.get("/data/upload/" + file.getOriginalFilename());

    file.transferTo(path);

    Files.deleteIfExists(path);

    return "success";
}

如果业务代码操作的是Multipart底层临时资源,或者某些自定义Multipart实现已经提前释放资源,就可能与Spring请求结束后的清理过程产生冲突。

正常情况下,不建议手动操作Spring内部管理的Multipart临时文件。

如果只是希望把上传文件保存到业务目录,可以使用:

Java
file.transferTo(destination);

保存完成后,由Spring负责Multipart资源生命周期的清理。

七、不要忘记关闭InputStream

如果项目没有直接使用:

Java
MultipartFile.transferTo(...)

而是手动读取文件流,那么需要正确关闭流。

例如:

Java
try (InputStream inputStream = file.getInputStream()) {
    // 处理文件
}

不要写成:

Java
InputStream inputStream = file.getInputStream();
// 使用inputStream

却没有关闭。

更推荐使用try-with-resources:

Java
try (InputStream inputStream = file.getInputStream();
     OutputStream outputStream = Files.newOutputStream(targetPath)) {

    byte[] buffer = new byte[8192];
    int length;

    while ((length = inputStream.read(buffer)) != -1) {
        outputStream.write(buffer, 0, length);
    }
}

这样可以确保文件流正常关闭。

在Linux环境中,如果文件一直被某个进程占用,也可能导致后续删除行为出现异常。

可以使用:

Bash
lsof | grep 文件名

检查哪个进程正在使用相关文件。

八、检查文件名处理逻辑

文件上传接口中,不能直接信任:

Java
file.getOriginalFilename()

例如:

Java
Path path = Paths.get("/data/upload/" + file.getOriginalFilename());

这种写法除了安全风险,也可能产生路径处理异常。

更推荐生成服务端文件名:

Java
String originalName = file.getOriginalFilename();
String suffix = "";

if (originalName != null && originalName.contains(".")) {
    suffix = originalName.substring(originalName.lastIndexOf("."));
}

String fileName = UUID.randomUUID() + suffix;

Path target = Paths.get("/data/upload", fileName);

file.transferTo(target);

这样可以减少特殊文件名、路径穿越以及文件覆盖等问题。

如果需要保留原始文件名,可以将其作为数据库字段保存,而不是直接把它作为服务器文件路径。

九、Spring Boot项目中的推荐上传实现

一个简单的文件上传Controller可以这样写:

Java
@RestController
@RequestMapping("/file")
public class FileController {

    private final Path uploadPath = Paths.get("/data/upload");

    @PostMapping("/upload")
    public ResponseEntity<String> upload(
            @RequestParam("file") MultipartFile file) throws IOException {

        if (file.isEmpty()) {
            return ResponseEntity.badRequest().body("文件不能为空");
        }

        Files.createDirectories(uploadPath);

        String originalName = file.getOriginalFilename();

        String suffix = "";
        if (originalName != null) {
            int index = originalName.lastIndexOf(".");
            if (index >= 0) {
                suffix = originalName.substring(index);
            }
        }

        String fileName = UUID.randomUUID() + suffix;

        Path target = uploadPath.resolve(fileName);

        file.transferTo(target);

        return ResponseEntity.ok("上传成功");
    }
}

这个实现将上传文件保存到业务目录,而不是让业务代码直接管理Multipart临时文件。

同时还应该根据实际需求增加文件类型校验、文件大小校验、文件名安全处理和访问权限控制。

十、使用Nginx时也要检查上传限制

如果Spring Boot前面还有Nginx,文件上传大小实际上受到多层配置影响。

例如Nginx:

Nginx
server {
    client_max_body_size 100M;
}

Spring Boot:

properties
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=110MB

如果Nginx只允许:

50MB

而Spring Boot允许:

100MB

上传超过50MB时,请求可能在到达Spring Boot之前就被Nginx拒绝。

因此完整的文件上传链路通常需要同时检查:

浏览器
  ↓
Nginx
  ↓
Spring Boot
  ↓
Multipart解析
  ↓
业务代码
  ↓
文件存储

每一层都有可能限制文件大小或影响文件处理。

十一、容器环境下尤其要关注临时目录

如果Spring Boot部署在Docker容器中,需要特别关注容器内的临时目录。

例如:

Bash
docker exec -it app-container sh

进入容器后检查:

Bash
df -h

以及:

Bash
ls -ld /tmp

如果容器设置了较小的临时存储空间,大文件上传时可能出现异常。

如果需要使用宿主机目录,可以通过Volume挂载:

Bash
docker run 
  -v /data/upload/tmp:/data/upload/tmp 
  your-app

然后配置:

properties
spring.servlet.multipart.location=/data/upload/tmp

这样可以将Multipart临时文件放到指定存储位置。

生产环境中还需要根据实际部署方式评估磁盘容量、容器临时存储、持久化目录以及清理策略。

十二、不要把cleanup异常和上传大小异常混为一谈

文件上传相关日志中经常会同时出现多个异常。

例如文件超过配置大小时,可能看到:

MaxUploadSizeExceededException

而Multipart资源清理失败时可能看到:

Failed to perform cleanup of multipart items

两者并不是同一个问题。

如果实际错误是:

Maximum upload size exceeded

应该检查:

properties
spring.servlet.multipart.max-file-size
spring.servlet.multipart.max-request-size

如果实际错误是:

Failed to perform cleanup of multipart items

则应该重点检查:

临时目录
文件权限
磁盘空间
文件句柄
流是否关闭
文件是否被其他程序占用
自定义Multipart配置

判断异常类型之后再采取对应措施,效率会高很多。

十三、生产环境排查Failed to perform cleanup的顺序

遇到这个问题时,可以按照下面的顺序检查。

首先查看完整异常:

Failed to perform cleanup of multipart items
    ↓
Caused by
    ↓
具体IOException或其他异常

然后检查服务器:

Bash
df -h
df -i

确认磁盘空间和inode是否正常。

接着检查Multipart临时目录:

Bash
ls -ld /tmp

如果使用自定义目录:

Bash
ls -ld /data/upload/tmp

然后确认Spring Boot进程运行用户:

Bash
ps -ef | grep java

检查该用户是否具有目标目录的正常访问权限。

如果怀疑文件仍然被占用,可以执行:

Bash
lsof | grep upload

如果部署在Docker中,再进入容器检查:

Bash
df -h

以及:

Bash
df -i

最后再检查应用代码,重点关注:

  • 是否手动删除Multipart临时文件;

  • 是否忘记关闭输入输出流;

  • 是否自定义MultipartResolver;

  • 是否修改了临时目录;

  • 是否在异步任务中继续使用MultipartFile;

  • 是否存在异常的文件传输逻辑。

十四、异步处理MultipartFile时需要特别小心

有些项目为了提高接口响应速度,会把上传文件交给异步任务处理。

例如:

Java
@PostMapping("/upload")
public String upload(@RequestParam MultipartFile file) {

    executor.execute(() -> {
        // 异步处理file
    });

    return "success";
}

这种方式存在一个潜在问题:Controller请求结束后,Spring可能已经开始清理Multipart资源,而异步任务仍然尝试读取MultipartFile。

更安全的方式是先将文件保存到业务目录,再把文件路径交给异步任务:

Java
Path target = uploadPath.resolve(fileName);

file.transferTo(target);

executor.execute(() -> {
    processFile(target);
});

这样异步任务操作的是已经保存好的业务文件,而不是依赖请求生命周期中的Multipart临时资源。

这一点在大文件上传、图片处理、视频转码、Excel解析等场景尤其重要。

十五、Spring Boot文件上传配置示例

常规项目可以从以下配置开始:

YAML
spring:
  servlet:
    multipart:
      enabled: true
      max-file-size: 100MB
      max-request-size: 110MB
      location: /data/upload/tmp

实际使用时,应根据服务器资源调整大小。

例如文件主要是图片:

YAML
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 20MB

如果主要上传大型压缩包:

YAML
spring:
  servlet:
    multipart:
      max-file-size: 500MB
      max-request-size: 550MB

但不能因为出现cleanup异常就无限制增大上传大小。上传文件越大,服务器需要承担的磁盘、内存、网络和处理压力也越高。

十六、常见错误处理方式

有几种处理方法看似有效,但实际上并不推荐。

第一种是直接删除/tmp:

Bash
rm -rf /tmp/*

这种方式可能误删其他程序正在使用的临时文件,生产服务器不建议直接执行。

第二种是直接设置:

Bash
chmod -R 777 /data/upload

虽然可能解决权限问题,但会造成不必要的安全风险。

第三种是只修改:

properties
spring.servlet.multipart.max-file-size

如果真正原因是磁盘空间或临时目录权限,那么修改上传大小不会解决cleanup异常。

第四种是忽略完整异常,只查看:

Failed to perform cleanup of multipart items

真正原因通常位于异常链的后面,因此必须结合Caused by分析。

十七、一个实用的排查思路

可以把整个问题归纳成四个方向:

Failed to perform cleanup
        ↓
① 看Caused by
        ↓
② 查临时目录
        ↓
③ 查磁盘和权限
        ↓
④ 查代码资源释放

如果是:

AccessDeniedException

优先检查目录权限。

如果是:

No space left on device

优先检查磁盘空间和inode。

如果是:

Too many open files

检查文件句柄和资源关闭情况。

如果是异步处理相关问题,则检查MultipartFile是否在HTTP请求结束后仍被使用。

如果同时存在Nginx、Docker或其他网关,还需要继续检查请求链路中的上传限制和临时存储空间。

十八、总结

Failed to perform cleanup of multipart items本质上是Spring Boot处理Multipart文件上传后,在清理临时资源阶段出现的问题。它不一定意味着文件本身没有上传成功,真正的故障原因通常需要结合后面的Caused by异常判断。

实际项目中,最值得优先检查的是Multipart临时目录权限、磁盘空间、inode、文件句柄以及代码中的输入输出流释放情况。同时,如果项目使用了异步文件处理、Docker、Nginx或者自定义Multipart配置,也应该纳入排查范围。

对于普通Spring Boot文件上传接口,建议让Spring负责Multipart临时资源的生命周期管理,业务代码只负责将文件保存到明确的业务目录,并通过try-with-resources正确管理手动创建的文件流。这样不仅能够减少Failed to perform cleanup of multipart items异常,也能让整个文件上传流程更加稳定、易于维护。