文件上传的原理是什么?multipart/form-data 是怎么回事、大文件上传怎么处理?
简化版
**文件上传的核心是「HTTP 用 multipart/form-data 编码把文件(和其他表单字段)打包在请求体里传给服务端」,服务端解析这个格式取出文件。**为什么用 multipart/form-data:普通表单用 application/x-www-form-urlencoded(键值对编码)只能传文本、不能传二进制文件;multipart/form-data 把请求体分成多个「部分(part)」,每个 part 有自己的头(含字段名、文件名、类型)和内容,用「分隔符(boundary)」隔开各个 part,能同时传文件(二进制)和普通字段。Servlet 3.0+ 的处理:用 request.getParts()(或 getPart(name))取出各 part,或用 @MultipartConfig 配置;Spring MVC 更简单:用 MultipartFile 参数直接接收(@RequestParam("file") MultipartFile file),调 file.getInputStream()/transferTo(dest) 保存。大文件上传要注意:① 别把整个文件读进内存(用流式处理、transferTo 边读边写);② 分片上传(前端把大文件切成小块分别上传、服务端合并,支持断点续传);③ 限制大小(max-file-size)防止超大文件打垮服务。核心:文件上传用 multipart/form-data 打包(boundary 分隔 part),Spring 用 MultipartFile 接收;大文件要流式、分片、限大小。
详细版
multipart/form-data 请求体结构:
Content-Type: multipart/form-data; boundary=----WebKitBoundary123
------WebKitBoundary123
Content-Disposition: form-data; name="username" ← 普通字段
Tom
------WebKitBoundary123
Content-Disposition: form-data; name="file"; filename="a.jpg" ← 文件字段
Content-Type: image/jpeg
<文件的二进制内容>
------WebKitBoundary123-- ← 结束分隔符
处理方式对比:
| 方式 | API |
|---|---|
| Servlet 3.0+ | request.getParts() / getPart(name),@MultipartConfig |
| Spring MVC | MultipartFile(@RequestParam(“file”) MultipartFile file) |
| 多文件 | MultipartFile[] / List |
// Spring MVC 文件上传(最简洁)
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file,
@RequestParam("username") String username) throws IOException {
if (file.isEmpty()) return "空文件";
String filename = file.getOriginalFilename(); // 原始文件名
long size = file.getSize(); // 大小
// ★ 流式保存(别读进内存)
file.transferTo(new File("/data/" + filename)); // 直接保存到磁盘
// 或 file.getInputStream() 流式处理
return "上传成功";
}
// 配置上传大小限制(application.yml)
// spring.servlet.multipart.max-file-size=10MB
// spring.servlet.multipart.max-request-size=50MB
⚠️ 大文件上传的第一原则是「别把整个文件读进内存」——否则大文件(几百 MB、GB)会直接把内存撑爆(OOM)。
MultipartFile的getBytes()会把整个文件读成 byte[](内存里),大文件绝对不能用;要用file.transferTo(dest)(直接保存到磁盘,框架内部流式处理)或file.getInputStream()(流式读,边读边写到目标,内存里只有缓冲区那点数据)。对于超大文件,还要考虑分片上传(chunked upload):前端把大文件切成很多小块(如每块 5MB),分别上传,服务端接收后按顺序合并——好处是①内存友好(每次只处理一小块)、②支持断点续传(某块失败只重传那块、或续传)、③可以并发上传多块加速、④能显示进度。另外要限制上传大小(max-file-size),否则恶意的超大文件会打垮服务。还要注意临时文件——上传的文件在解析时可能先存到临时目录,要及时清理(Spring 处理完自动清)。所以大文件上传 = 流式(不读内存)+ 分片(切块合并、断点续传)+ 限大小。
完整版教学
一、为什么要 multipart/form-data
先理解为什么文件上传要用 multipart/form-data:
普通表单提交(application/x-www-form-urlencoded):
把表单字段编码成键值对:username=Tom&age=18
→ 只能传文本(键值对、URL 编码)
→ 不能传二进制文件(文件是二进制、有各种字节,没法编码成键值对)
文件上传的需求:
要传"文件(二进制)+ 其他字段"
→ 普通表单编码搞不定文件
multipart/form-data 的方案:
把请求体分成多个"部分(part)":
每个字段一个 part(普通字段、文件字段)
每个 part 有自己的头(字段名、文件名、类型)和内容
用"分隔符(boundary)"隔开各 part
→ 能同时传文件(二进制内容)和普通字段
→ 二进制原样传(不用编码)
为什么叫 multipart(多部分):
请求体由多个 part 组成(每个字段/文件一个 part)
→ "多部分"的表单数据
所以文件上传用 multipart/form-data(多部分、能传二进制文件)
文件上传要用 multipart/form-data——普通表单(application/x-www-form-urlencoded)把字段编码成键值对,只能传文本、不能传二进制文件。文件上传要传「文件(二进制)+其他字段」,普通表单搞不定。multipart/form-data 的方案:把请求体分成多个「部分(part)」(每个字段一个 part,每个 part 有头(字段名/文件名/类型)和内容,用分隔符 boundary 隔开),能同时传文件(二进制原样)和普通字段。理解「文件上传用 multipart/form-data:普通表单只能传文本(键值对编码)不能传二进制文件;multipart 把请求体分多个 part(每个字段一个,有头和内容,boundary 分隔),能传二进制文件+普通字段」,就理解了为什么要 multipart/form-data。
二、multipart/form-data 的结构
理解 multipart/form-data 请求体的具体结构:
请求头:
Content-Type: multipart/form-data; boundary=----Boundary123
→ 声明是 multipart、boundary 是分隔符(随机生成、唯一)
请求体(多个 part,用 boundary 分隔):
------Boundary123 ← boundary 开始一个 part
Content-Disposition: form-data; name="username" ← part 头(普通字段)
Tom ← part 内容
------Boundary123 ← 下一个 part
Content-Disposition: form-data; name="file"; filename="a.jpg" ← 文件 part
Content-Type: image/jpeg ← 文件类型
<文件的二进制内容> ← 文件内容(二进制)
------Boundary123-- ← 结束(boundary + --)
关键元素:
boundary:分隔符——隔开各 part(每个 part 前有 --boundary)
→ 服务端靠 boundary 切分各 part
Content-Disposition:part 的头
name:字段名
filename:文件名(有 filename 的是文件 part)
Content-Type:part 的内容类型(文件的类型)
服务端解析:
① 从 Content-Type 拿到 boundary
② 用 boundary 切分请求体成多个 part
③ 每个 part:解析头(name、filename)+ 内容
→ 取出普通字段和文件
所以 multipart/form-data = boundary 分隔的多个 part(字段+文件)
multipart/form-data 请求体结构:请求头 Content-Type: multipart/form-data; boundary=xxx(声明 multipart + 分隔符 boundary);请求体是多个 part 用 boundary 分隔(每个 part 前有 --boundary,part 有头 Content-Disposition(name 字段名、filename 文件名——有 filename 的是文件 part)和内容,最后 --boundary-- 结束)。服务端解析:从 Content-Type 拿 boundary → 用它切分请求体成多个 part → 每个 part 解析头和内容 → 取出字段和文件。理解「multipart 结构:请求头 Content-Type 声明 boundary;请求体多个 part 用 boundary 分隔(part 有 Content-Disposition 头 name/filename+内容);服务端用 boundary 切分 part 解析取出字段和文件」,就掌握了 multipart 的结构。
三、Servlet 处理文件上传
Servlet 3.0+ 内置支持文件上传:
Servlet 3.0+ 的文件上传支持:
① @MultipartConfig 注解(在 Servlet 上):
@MultipartConfig(maxFileSize = 10*1024*1024, ...)
→ 声明这个 Servlet 支持 multipart、配置大小限制等
② request.getParts() / getPart(name):
Collection<Part> parts = request.getParts(); // 所有 part
Part filePart = request.getPart("file"); // 某个 part(文件)
→ Part 是每个部分(字段/文件)的抽象
处理文件 part:
Part filePart = request.getPart("file");
String filename = filePart.getSubmittedFileName(); // 文件名
InputStream in = filePart.getInputStream(); // 文件流
// 保存文件(流式)
filePart.write("/data/" + filename); // 或用流保存
Servlet 3.0 之前(麻烦):
用 Apache Commons FileUpload 库自己解析 multipart
→ 手动切 part、解析(繁琐)
所以 Servlet 3.0+ 用 @MultipartConfig + getParts 处理文件上传
比早期用 Commons FileUpload 简单
Servlet 3.0+ 内置文件上传支持:① @MultipartConfig 注解(在 Servlet 上,声明支持 multipart、配大小限制)、② request.getParts()/getPart(name)(取所有 part 或某个 part,Part 是每个部分的抽象)。处理文件 part:getPart("file") → getSubmittedFileName() 文件名、getInputStream() 文件流、write(path) 保存。Servlet 3.0 之前用 Apache Commons FileUpload 手动解析(繁琐)。理解「Servlet 3.0+文件上传:@MultipartConfig 声明支持+配大小、request.getParts()/getPart 取 part(Part 是每部分抽象);处理文件 part:getSubmittedFileName/getInputStream/write;3.0 前用 Commons FileUpload 手动解析」,就掌握了 Servlet 处理文件上传。
四、Spring MVC 的 MultipartFile
Spring MVC 用 MultipartFile 让文件上传更简单:
Spring MVC 的 MultipartFile:
用 @RequestParam("file") MultipartFile file 直接接收上传的文件
→ Spring 帮你解析 multipart、封装成 MultipartFile
MultipartFile 的常用方法:
getOriginalFilename():原始文件名
getSize():文件大小
getContentType():文件类型
getInputStream():文件的输入流(流式读,推荐)
getBytes():文件字节数组(★ 读进内存,大文件别用)
transferTo(File dest):保存到磁盘(流式,推荐)
isEmpty():是否为空
用法:
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) return "空";
file.transferTo(new File("/data/" + file.getOriginalFilename())); // 保存
return "ok";
}
多文件:
@RequestParam("files") MultipartFile[] files
或 List<MultipartFile>
配置(Spring Boot):
spring.servlet.multipart.max-file-size=10MB // 单文件大小
spring.servlet.multipart.max-request-size=50MB // 整个请求大小
spring.servlet.multipart.enabled=true // 默认开启
底层:
Spring 用 MultipartResolver 解析 multipart
(StandardServletMultipartResolver 基于 Servlet 3.0)
所以 Spring MVC 用 MultipartFile 接收文件(transferTo 保存,别用 getBytes)
Spring MVC 用 MultipartFile 简化文件上传——@RequestParam("file") MultipartFile file 直接接收(Spring 帮你解析 multipart)。常用方法:getOriginalFilename/getSize/getContentType/getInputStream(流式读,推荐)/getBytes(读进内存,大文件别用)/transferTo(dest)(保存到磁盘,流式推荐)。多文件用 MultipartFile[]。配置:spring.servlet.multipart.max-file-size/max-request-size。底层 MultipartResolver 解析。理解「Spring MVC MultipartFile:@RequestParam MultipartFile file 直接接收;方法 getOriginalFilename/getInputStream(流式推荐)/transferTo(保存推荐)/getBytes(读内存大文件别用);多文件 MultipartFile[];配置 max-file-size」,就掌握了 MultipartFile。
五、大文件上传
大文件上传要特殊处理——流式、分片、限大小:
大文件上传的三个关键:
① 流式处理(别读进内存):
✗ file.getBytes():把整个文件读成 byte[](内存)→ 大文件 OOM
✓ file.transferTo(dest):框架流式保存到磁盘(不占内存)
✓ file.getInputStream() + 流式写:边读边写(内存只有缓冲区)
→ 大文件绝对不能读进内存
② 分片上传(chunked upload):
前端把大文件切成很多小块(如每块 5MB),分别上传
服务端接收每块 → 按顺序合并成完整文件
好处:
- 内存友好(每次只处理一小块)
- 断点续传(某块失败只重传那块、或从断点续)
- 并发上传多块加速
- 显示进度
流程:
1. 前端切片(file.slice(start, end))
2. 每块带(文件唯一标识 + 块序号)上传
3. 服务端存每块(临时)
4. 所有块传完 → 服务端按序号合并
(断点续传:记录已上传的块,续传缺的块)
③ 限制大小(防超大文件打垮):
max-file-size / max-request-size
→ 超过限制拒绝(防止恶意超大文件耗尽资源)
④ 临时文件清理:
上传文件解析时可能先存临时目录
→ 处理完及时清理(Spring 自动清)
所以大文件上传 = 流式(不读内存) + 分片(切块合并/断点续传) + 限大小
大文件上传三个关键:① 流式处理(别读进内存)——getBytes() 读整个文件到内存大文件 OOM,用 transferTo(dest)(框架流式保存)或 getInputStream() 流式写(内存只有缓冲区);② 分片上传(chunked upload)——前端切成小块分别上传、服务端按序合并(好处:内存友好、断点续传、并发加速、显示进度;流程:前端切片→每块带文件标识+序号上传→服务端存每块→传完合并);③ 限制大小(max-file-size/max-request-size 防超大文件打垮);④ 临时文件清理。理解「大文件上传:①流式(别用 getBytes 读内存 OOM,用 transferTo/getInputStream)②分片上传(前端切小块分别传、服务端按序合并,内存友好+断点续传+并发+进度)③限大小(max-file-size 防打垮)④临时文件清理」,就掌握了大文件上传。
六、实践与总结
总结文件上传的实践:
实践建议:
① 普通文件上传 → Spring MVC 的 MultipartFile(简洁)
transferTo 保存(别用 getBytes,流式)
② 大文件 → 流式 + 分片上传(切块合并、断点续传)
③ 限制大小(max-file-size / max-request-size)
④ 校验(文件类型、大小、内容——防恶意文件)
⑤ 存储(本地磁盘 / 对象存储 OSS / 分布式文件系统)
安全注意:
① 校验文件类型(别只信扩展名、检查内容/魔数)
② 限制大小(防超大文件)
③ 文件名处理(防路径穿越 ../、防覆盖)
④ 存储隔离(上传的文件别能被当脚本执行)
⑤ 病毒扫描(敏感场景)
现代做法:
① 大文件/生产 → 直传对象存储(前端拿签名直传 OSS,不经服务端)
→ 减轻服务端压力、更快
② 分片上传 → 大文件断点续传(很多网盘、视频上传用)
核心总结:
文件上传用 multipart/form-data(boundary 分隔 part,传文件+字段)
Spring 用 MultipartFile 接收(transferTo 保存,别用 getBytes)
大文件:流式(不读内存)+ 分片(切块合并/断点续传)+ 限大小
安全:校验类型/大小、防路径穿越、存储隔离
文件上传实践:普通文件用 MultipartFile(transferTo 保存别用 getBytes)、大文件用流式+分片、限大小、校验(类型/大小/内容)、存储(本地/OSS/分布式)。安全:校验文件类型(检查内容不只扩展名)、限大小、文件名处理(防路径穿越../)、存储隔离(别被当脚本执行)、病毒扫描。现代做法:大文件直传对象存储(前端签名直传 OSS 不经服务端)、分片上传断点续传。理解「实践:普通用 MultipartFile(transferTo)/大文件流式+分片/限大小/校验/存储;安全:校验类型(查内容)/限大小/防路径穿越/存储隔离;现代:直传 OSS/分片断点续传」,就掌握了文件上传的实践。
记忆钩子:「文件上传用 multipart/form-data(普通表单 application/x-www-form-urlencoded 只能传文本不能传二进制文件);multipart 把请求体分多个 part(每个字段/文件一个,有 Content-Disposition 头 name/filename+内容)用 boundary 分隔,能传二进制文件+字段;服务端用 boundary 切分 part 解析;Servlet 3.0+用 @MultipartConfig+request.getParts()/getPart;★Spring MVC 用 MultipartFile(@RequestParam MultipartFile file),transferTo(dest)保存/getInputStream 流式(别用 getBytes 读内存大文件 OOM);★大文件上传:①流式(别读内存)②分片上传(前端切小块分别传、服务端按序合并,内存友好+断点续传+并发+进度)③限大小(max-file-size 防打垮);安全:校验类型(查内容不只扩展名)/防路径穿越../;现代直传 OSS」。
七、常见误区与追问
- 误区:普通表单能上传文件。 普通表单用 application/x-www-form-urlencoded 编码,只能传文本(键值对),不能传二进制文件;文件上传要用 multipart/form-data 编码(enctype=“multipart/form-data”),它把请求体分成多个 part、能传二进制文件和普通字段。
- 误区:用 MultipartFile.getBytes() 读取上传的文件。 大文件绝对不能用 getBytes()——它把整个文件读成 byte[] 放内存,大文件(几百 MB、GB)会 OOM;应该用 transferTo(dest)(框架流式保存到磁盘)或 getInputStream()(流式读、边读边写),内存里只有缓冲区那点数据。
- 误区:大文件直接一次性上传。 大文件应该分片上传——前端把文件切成小块(如每块 5MB)分别上传、服务端按顺序合并;好处是内存友好(每次处理一小块)、支持断点续传(失败只重传那块)、可并发加速、能显示进度;一次性上传大文件容易超时、失败要全部重来、占内存。
- 误区:只校验文件扩展名就安全了。 不够——扩展名可以伪造(把 .exe 改成 .jpg);要检查文件内容/魔数(文件头的特征字节)判断真实类型;还要限制大小、处理文件名(防路径穿越 ../)、存储隔离(上传目录别能执行脚本);文件上传是常见的攻击点。
- 追问:文件上传为什么要用 multipart/form-data? 因为普通表单编码(application/x-www-form-urlencoded)只能把字段编码成键值对文本,无法传输二进制文件(文件是二进制、有各种字节,没法编码成键值对);multipart/form-data 把请求体分成多个「部分(part)」,每个字段/文件一个 part,每个 part 有自己的头(字段名、文件名、类型)和内容,用分隔符(boundary)隔开各 part,这样能同时传输二进制文件(原样传)和普通字段。
- 追问:大文件上传怎么处理才不会 OOM? 关键是「别把整个文件读进内存」:① 用 MultipartFile.transferTo(dest) 直接保存到磁盘(框架内部流式处理,不占内存)或 getInputStream() 流式读(边读边写到目标,内存只有缓冲区);② 对于超大文件用分片上传(前端切成小块分别上传、服务端按序合并,每次只处理一小块,内存友好);③ 限制上传大小(max-file-size)防止恶意超大文件;④ 避免用 getBytes()(会把整个文件读成 byte[] 放内存)。
- 追问:分片上传是怎么实现断点续传的? 分片上传把大文件切成很多小块(每块有文件的唯一标识 + 块序号),分别上传;服务端记录每个文件已经上传成功的块;如果上传中断,重新开始时,前端(或服务端)先查询已上传的块,只上传还没传成功的块(断点续传),而不用从头重传整个文件;所有块传完后,服务端按块序号合并成完整文件;这样既支持断点续传、又内存友好、还能并发上传多块加速。
八、加强记忆
文件上传的核心是「HTTP 用 multipart/form-data 编码把文件和表单字段打包在请求体里」——普通表单(application/x-www-form-urlencoded)只能传文本、不能传二进制文件;multipart/form-data 把请求体分成多个「部分(part)」(每个字段/文件一个 part,有头 Content-Disposition(name 字段名、filename 文件名)和内容,用分隔符 boundary 隔开),能同时传文件(二进制)和字段。服务端用 boundary 切分 part 解析。Servlet 3.0+ 用 @MultipartConfig + request.getParts()/getPart;Spring MVC 用 MultipartFile(@RequestParam("file") MultipartFile file,transferTo(dest) 保存 / getInputStream() 流式,别用 getBytes() 读进内存)。大文件上传要注意:① 流式处理(别读进内存,否则 OOM)、② 分片上传(前端切小块分别上传、服务端按序合并,内存友好 + 断点续传 + 并发加速 + 显示进度)、③ 限制大小(max-file-size 防打垮)、④ 临时文件清理。安全:校验文件类型(检查内容/魔数不只扩展名)、限大小、防路径穿越(../)、存储隔离;现代大文件常直传对象存储(OSS)。一句话「文件上传用 multipart/form-data(boundary 分隔多个 part,传文件+字段,普通表单只能传文本);Spring 用 MultipartFile(transferTo 保存/getInputStream 流式,别用 getBytes 读内存 OOM);大文件:流式(不读内存)+分片(切块合并/断点续传)+限大小;安全校验类型查内容/防路径穿越;现代直传 OSS」。