乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 SpringMVC概述与DispatcherServlet

    • 本章导读:Spring MVC概述与DispatcherServlet
    • Spring MVC 是什么
    • MVC 设计模式
    • 前端控制器模式
    • DispatcherServlet
    • 核心组件协作
  • 第2章 控制器与请求映射

    • 本章导读:控制器与请求映射
    • Controller
    • RestController
    • RequestMapping
    • GetMapping
    • PostMapping
    • PutMapping
    • DeleteMapping
    • PathVariable
    • RESTful
    • 请求映射原理
  • 第3章 请求参数获取与转换

    • 本章导读:请求参数获取与转换
    • RequestParam
    • RequestBody
    • RequestHeader
    • CookieValue
    • Model
    • ModelAttribute
    • 数据绑定原理
    • 数据校验
  • 第4章 响应数据与视图解析

    • 本章导读:响应数据与视图解析
    • ResponseBody
    • ResponseEntity
    • ModelAndView
    • ViewResolver
    • HttpMessageConverter
    • forward与redirect
  • 第5章 拦截器过滤器与跨域

    • 本章导读:拦截器、过滤器与跨域
    • HandlerInterceptor
    • WebMvcConfigurer
    • CrossOrigin
    • 登录验证实战
  • 第6章 文件上传与异常处理

    • 本章导读:文件上传与异常处理
    • MultipartFile
    • 文件下载
    • ExceptionHandler
    • ControllerAdvice
    • RestControllerAdvice
    • ResponseStatus
  • 第7章 高级特性与最佳实践

    • 本章导读:高级特性与最佳实践
    • SessionAttributes
    • SessionAttribute
    • RedirectAttributes
    • MockMvc测试
    • 国际化
    • 最佳实践
  • 第8章 扩展与异步机制

    • 本章导读:扩展与异步机制
    • 异步请求处理
    • 自定义参数解析器
    • 内容协商

MultipartFile

本章聚焦 Spring MVC 的文件上传处理。员工管理系统中,头像上传、合同附件上传、批量导入 Excel 等场景都需要处理 multipart/form-data 请求。MultipartFile 是 Spring MVC 封装上传文件的标准接口,它屏蔽了底层 Servlet 3.0 Part API 的复杂性,让开发者以面向对象的方式操作上传文件。

本章与全局的关系:前面章节讲解了普通 HTTP 请求的参数绑定和消息转换,本章讲解multipart 请求的特殊处理流程。文件上传涉及请求体解析、临时文件管理、磁盘写入等独立机制。


定义与作用

MultipartFile 是 Spring MVC 提供的上传文件封装接口,代表一个从 multipart/form-data 请求中解析出的文件项。它提供了获取文件名、内容类型、文件大小、输入流、字节数组等方法,以及最常用 transferTo(File dest) 方法把文件保存到磁盘。

核心职责:

  1. 封装原始文件数据:屏蔽 HttpServletRequest.getPart() 或 Commons FileUpload 的底层差异
  2. 提供便捷操作方法:获取元信息、读取内容、保存到磁盘
  3. 支持单文件和多文件:通过 @RequestParam("file") MultipartFile file 或 MultipartFile[] files 绑定

生活类比:公司前台签收快递

想象飞翔科技的前台:

  • 快递员(浏览器)送来一个包裹(multipart/form-data 请求),里面可能有多个物品(多个文件 + 普通表单字段)
  • 前台(Spring MVC 的 MultipartResolver)拆开包裹,把每个物品贴上标签(封装成 MultipartFile)
  • 你(Controller 方法)只需要说"把标着'file'的那个包裹给我",前台就递过来一个 MultipartFile
  • 你可以查看包裹上的信息(getOriginalFilename, getSize),也可以直接把它搬到你的储物柜(transferTo)

关键认知:MultipartFile 只是内存或临时文件中的数据引用,不是持久化存储。如果不调用 transferTo() 或读取 InputStream 保存,请求结束后临时文件会被清理。


核心原理

文件上传流程

multipart/form-data 解析过程

请求体示例:

POST /upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
Content-Length: 1234

------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="description"

员工头像上传
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="file"; filename="avatar.png"
Content-Type: image/png

[二进制图片数据]
------WebKitFormBoundary7MA4YWxk--

适用位置与常用属性

MultipartFile 作为 Controller 方法的参数类型,通过 @RequestParam 或 @RequestPart 绑定:

绑定方式示例说明
@RequestParam("file")@RequestParam("file") MultipartFile file最常用,按表单字段名绑定
@RequestPart("file")@RequestPart("file") MultipartFile file与 @RequestParam 类似,语义更明确
无注解(需配合配置)MultipartFile file字段名与参数名一致时可省略

MultipartFile 接口方法

方法返回值作用
getName()String表单字段名(如 "file")
getOriginalFilename()String原始文件名(如 "avatar.png")
getContentType()String文件 MIME 类型(如 "image/png")
getSize()long文件大小(字节)
getBytes()byte[]获取文件内容的字节数组
getInputStream()InputStream获取文件内容的输入流
transferTo(File dest)void保存到指定文件路径
isEmpty()boolean是否为空(未选择文件)

完整示例

场景

飞翔科技员工管理系统需要支持:

  1. 单文件上传:员工上传个人头像
  2. 多文件上传:批量上传合同附件
  3. 文件信息校验:检查文件类型和大小

项目结构

employee-web/
├── src/main/java/
│   └── com/feixiang/web/
│       └── controller/
│           └── FileUploadController.java
├── src/main/resources/
│   └── application.properties
└── uploads/  (运行时创建)

配置

# application.properties
# 启用 multipart 解析
spring.servlet.multipart.enabled=true
# 单个文件最大大小
spring.servlet.multipart.max-file-size=10MB
# 整个请求最大大小
spring.servlet.multipart.max-request-size=100MB
# 文件写入磁盘的阈值(超过则先写临时文件)
spring.servlet.multipart.file-size-threshold=2KB
# 临时文件目录
spring.servlet.multipart.location=/tmp/feixiang

Controller 实现

// FileUploadController.java
package com.feixiang.web.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

@RestController
@RequestMapping("/api/files")
public class FileUploadController {

    @Value("${upload.path:/var/feixiang/uploads}")
    private String uploadPath;

    // 单文件上传
    @PostMapping("/upload")
    public UploadResult uploadFile(
            @RequestParam("file") MultipartFile file,
            @RequestParam("description") String description) throws IOException {

        // 校验
        if (file.isEmpty()) {
            throw new IllegalArgumentException("文件不能为空");
        }
        if (file.getSize() > 10 * 1024 * 1024) {
            throw new IllegalArgumentException("文件大小超过10MB限制");
        }

        // 生成唯一文件名
        String originalFilename = file.getOriginalFilename();
        String extension = originalFilename != null 
            ? originalFilename.substring(originalFilename.lastIndexOf(".")) 
            : "";
        String newFilename = UUID.randomUUID().toString() + extension;

        // 保存到磁盘
        File destDir = new File(uploadPath);
        if (!destDir.exists()) {
            destDir.mkdirs();
        }
        File destFile = new File(destDir, newFilename);
        file.transferTo(destFile);

        return new UploadResult(newFilename, file.getSize(), description, "上传成功");
    }

    // 多文件上传
    @PostMapping("/batch-upload")
    public List<UploadResult> uploadMultipleFiles(
            @RequestParam("files") MultipartFile[] files) throws IOException {

        List<UploadResult> results = new ArrayList<>();
        
        for (MultipartFile file : files) {
            if (file.isEmpty()) continue;
            
            String newFilename = UUID.randomUUID().toString() + "_" + file.getOriginalFilename();
            File destFile = new File(uploadPath, newFilename);
            file.transferTo(destFile);
            
            results.add(new UploadResult(newFilename, file.getSize(), "", "上传成功"));
        }
        
        return results;
    }

    // 内部 DTO
    public static class UploadResult {
        private String filename;
        private long size;
        private String description;
        private String message;

        public UploadResult(String filename, long size, String description, String message) {
            this.filename = filename;
            this.size = size;
            this.description = description;
            this.message = message;
        }

        // getters
        public String getFilename() { return filename; }
        public long getSize() { return size; }
        public String getDescription() { return description; }
        public String getMessage() { return message; }
    }
}

HTTP 请求示例 1:单文件上传

$ curl -X POST http://localhost:8080/api/files/upload \
  -F "file=@/path/to/avatar.png" \
  -F "description=员工头像"

响应:

{
  "filename": "a1b2c3d4-e5f6-7890-abcd-ef1234567890.png",
  "size": 24576,
  "description": "员工头像",
  "message": "上传成功"
}

流程解析:

  1. curl 构造 multipart/form-data 请求体,boundary 自动生成为 ----WebKitFormBoundary...
  2. 请求体包含两个 part:file(二进制图片数据)和 description(纯文本)
  3. Spring Boot 的 StandardServletMultipartResolver 解析请求体
  4. file 部分被封装为 StandardMultipartFile,绑定到方法参数
  5. description 作为普通表单字段绑定到 String 参数
  6. transferTo() 把临时文件移动到目标目录

HTTP 请求示例 2:多文件上传

$ curl -X POST http://localhost:8080/api/files/batch-upload \
  -F "files=@/path/to/contract1.pdf" \
  -F "files=@/path/to/contract2.pdf" \
  -F "files=@/path/to/contract3.pdf"

响应:

[
  {
    "filename": "uuid1_contract1.pdf",
    "size": 102400,
    "description": "",
    "message": "上传成功"
  },
  {
    "filename": "uuid2_contract2.pdf",
    "size": 204800,
    "description": "",
    "message": "上传成功"
  },
  {
    "filename": "uuid3_contract3.pdf",
    "size": 153600,
    "description": "",
    "message": "上传成功"
  }
]

HTTP 请求示例 3:空文件上传失败

$ curl -X POST http://localhost:8080/api/files/upload \
  -F "file=@/dev/null" \
  -F "description=测试"

响应:

{
  "error": "文件不能为空"
}

状态码:400 Bad Request


易错场景与面试考点

误区一:transferTo 后还能读取文件

错误代码:

file.transferTo(new File("/uploads/avatar.png"));
byte[] bytes = file.getBytes();  // 可能抛异常!

错误现象:IllegalStateException: File has been moved - cannot be read again

纠正:transferTo() 是移动操作(对于某些实现是复制后删除),调用后 MultipartFile 底层的临时文件可能已不存在。如果需要既保存又读取,应先 getInputStream() 或 getBytes() 读取,再自行写入目标文件。

误区二:文件名包含路径分隔符导致安全问题

错误代码:

String filename = file.getOriginalFilename();
File dest = new File(uploadPath, filename);  // 危险!
file.transferTo(dest);

攻击场景:用户上传文件,文件名设为 ../../../etc/passwd,可能导致文件被写入系统关键目录。

纠正:必须清理文件名,去除路径信息:

String filename = file.getOriginalFilename();
if (filename != null) {
    filename = filename.substring(filename.lastIndexOf("/") + 1)
                       .substring(filename.lastIndexOf("\\") + 1);
}
String newFilename = UUID.randomUUID().toString() + "_" + filename;

误区三:Spring Boot 2.x 与 Commons FileUpload 混用

错误认知:"Spring Boot 项目需要引入 commons-fileupload 依赖才能上传文件。"

纠正:Spring Boot 2.x 基于 Servlet 3.0+,默认使用 StandardServletMultipartResolver(基于 Servlet 3.0 原生 Part API),不需要 Commons FileUpload。只有传统 Spring MVC + Servlet 2.x 环境才需要 commons-fileupload。

面试高频:MultipartFile 和 File 的区别

对比维度MultipartFileFile
来源HTTP 请求体中的临时数据磁盘上的持久化文件
生命周期请求结束后临时文件被清理长期存在
操作方式通过接口方法(transferTo, getBytes)通过 java.io.File API
是否可重复读取取决于实现,transferTo 后通常不可可以

标准回答:MultipartFile 是 Spring MVC 对 HTTP multipart 请求中文件项的封装,数据可能存在于内存或临时文件中。File 是 Java IO 对磁盘文件的抽象。MultipartFile 需要通过 transferTo(File) 才能持久化到磁盘。

面试高频:大文件上传的内存问题

标准回答:Spring Boot 的 multipart 配置中有 file-size-threshold 参数:

  • 小于阈值:文件保存在内存中,速度快但占用堆内存
  • 大于阈值:文件先写入临时文件(磁盘),不占用堆内存

大文件上传时,应调低 file-size-threshold(如 0,表示立即写磁盘),并设置合理的 max-file-size 和 max-request-size 防止 OOM 或磁盘耗尽。


小结

MultipartFile 是 Spring MVC 的文件上传封装接口,通过 @RequestParam("file") MultipartFile file 绑定 multipart/form-data 请求中的文件项。transferTo(File) 是最常用的持久化方法,但调用后不可再次读取。单文件用 MultipartFile,多文件用 MultipartFile[] 或 List<MultipartFile>。Spring Boot 2.x 默认使用 Servlet 3.0 原生解析,无需额外依赖。

本章与全局的关系:本章讲解了文件上传的请求处理。下一章"异常处理"将讲解当文件上传校验失败、磁盘写入失败等异常发生时,如何在 Controller 层面统一处理。

上一页
本章导读:文件上传与异常处理
下一页
文件下载