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) 方法把文件保存到磁盘。
核心职责:
- 封装原始文件数据:屏蔽
HttpServletRequest.getPart()或 Commons FileUpload 的底层差异 - 提供便捷操作方法:获取元信息、读取内容、保存到磁盘
- 支持单文件和多文件:通过
@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 | 是否为空(未选择文件) |
完整示例
场景
飞翔科技员工管理系统需要支持:
- 单文件上传:员工上传个人头像
- 多文件上传:批量上传合同附件
- 文件信息校验:检查文件类型和大小
项目结构
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": "上传成功"
}
流程解析:
- curl 构造 multipart/form-data 请求体,boundary 自动生成为
----WebKitFormBoundary... - 请求体包含两个 part:
file(二进制图片数据)和description(纯文本) - Spring Boot 的
StandardServletMultipartResolver解析请求体 file部分被封装为StandardMultipartFile,绑定到方法参数description作为普通表单字段绑定到 String 参数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 的区别
| 对比维度 | MultipartFile | File |
|---|---|---|
| 来源 | 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 层面统一处理。