你以为文件上传就是收个 MultipartFile?那你敢不敢让用户传 2GB 试试?
🏆本文收录于《滚雪球学SpringBoot 3》:
https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。
演示环境说明:
- 开发工具:IDEA 2021.3
- JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
- Spring Boot版本:3.5.4(于25年7月24日发布)
- Maven版本:3.8.2 (或更高)
- Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
- 操作系统:Windows 11
前言:别把“文件”当成普通参数,它是资源消耗怪兽🦖
HTTP 请求里塞个 JSON,顶多几 KB~几百 KB;但文件不同,它天然可能是几十 MB、几 GB,甚至更大。你的代码每多一次“把流读成 byte[]”的冲动,就离 OOM 更近一步。
所以本文我会把话说得很直:
- 能流式就别数组
- 能落对象存储就别落本机
- 能限制就别放任
- 能校验就别相信用户(“用户”这个词,本质上等同于“未知风险源”🙂)
1. MultipartFile 接口详解:哪些方法看似方便,其实很危险?
MultipartFile 是 Spring MVC 里对上传文件的抽象,核心方法包括:
getName():表单字段名getOriginalFilename():客户端原始文件名(⚠️不可信)getSize():大小getInputStream():拿到文件内容的输入流getBytes():直接拿 byte[](⚠️大文件慎用)transferTo(File/Path):把接收到的文件转存到目标位置
这些都在官方 Javadoc 里写得很清楚,而且还明确提醒:getOriginalFilename() 是客户端提供的,不要盲目使用,避免路径穿越等风险(例如 .. 之类的字符)。
1.1 你真正应该优先用的:getInputStream() / transferTo()
getInputStream():最适合“流式”处理(例如边读边写到磁盘/对象存储)transferTo():适合“先落临时目录再处理”的路径(但要注意只调用一次、以及容器临时存储差异)
1.2 尽量少用的:getBytes()
getBytes() 会把整个文件内容读进内存,Javadoc 也明确这是“内容以字节数组返回”。
小文件当然没问题;但一旦碰到大文件,这就是“内存自杀按钮”。
2. 流式上传与下载:让大文件“过路”,别让它“住进内存”
这里我先按**Spring MVC(Servlet 栈)**写示例,因为它最常见。后面会补一句 WebFlux 的“真·顺序流式”玩法。
2.1 流式上传:MultipartFile ➜ InputStream ➜ 输出(磁盘或对象存储)
✅ 推荐:边读边写(不落 byte[])
@RestController
@RequestMapping("/files")
public class FileController {
private final Path storageRoot = Paths.get("/data/files"); // 示例:你要确保权限与容量
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Map<String, Object> upload(@RequestParam("file") MultipartFile file) throws IOException {
// 1) 基础校验:别太信任用户
if (file.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "文件为空?你这是在逗我吗🙂");
}
// 原始文件名不可信:只取文件名部分,或者直接生成 UUID
String safeName = UUID.randomUUID() + "_" + sanitizeFilename(file.getOriginalFilename());
Path target = storageRoot.resolve(safeName).normalize();
Files.createDirectories(storageRoot);
// 2) 流式复制:InputStream -> Files.newOutputStream
try (InputStream in = file.getInputStream();
OutputStream out = Files.newOutputStream(target, StandardOpenOption.CREATE_NEW)) {
in.transferTo(out); // JDK 9+,底层是分块复制
}
return Map.of(
"name", safeName,
"size", file.getSize(),
"contentType", file.getContentType()
);
}
private String sanitizeFilename(String original) {
if (original == null) return "unknown";
// 只保留文件名,干掉路径;再做字符白名单/替换
String name = Paths.get(original).getFileName().toString();
return name.replaceAll("[\\\\/\\r\\n\\t]", "_");
}
}
为什么这算“最佳实践”?
因为你没有调用 getBytes(),而是用 getInputStream() 走流,符合 Spring MultipartFile 的设计预期。
小提醒(很现实):
- 你要限制并发上传,否则 IO 会被打满
- 大文件上传更建议走对象存储(MinIO/S3)或前端直传(预签名 URL)
- 本机落盘只是“最小可用”,不是“最终归宿”
2.2 流式下载:InputStream ➜ HTTP 响应(StreamingResponseBody)
下载接口最常见的坑:
byte[] data = Files.readAllBytes(path)(然后你就等着内存报警吧)
正确做法是:边读边写给客户端。
@GetMapping("/download/{name}")
public ResponseEntity<StreamingResponseBody> download(@PathVariable String name) throws IOException {
Path file = Paths.get("/data/files").resolve(name).normalize();
if (!Files.exists(file)) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "文件不存在");
}
long size = Files.size(file);
String contentType = Files.probeContentType(file);
if (contentType == null) contentType = "application/octet-stream";
StreamingResponseBody body = outputStream -> {
try (InputStream in = Files.newInputStream(file)) {
in.transferTo(outputStream);
}
};
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + name + "\"")
.contentType(MediaType.parseMediaType(contentType))
.contentLength(size)
.body(body);
}
这样做的好处很朴素:文件不会整坨塞进内存,服务器只是做“管道”。
(当然,网速慢的用户会占用连接更久,所以你还需要超时/限流/并发控制,这属于“真实世界的代价”。)
2.3 进阶一句:如果你真要“顺序流式解析 multipart”,WebFlux 更像你要的
Spring WebFlux 允许用 @RequestBody Flux<PartEvent>(或 Kotlin 的 Flow)来顺序、流式读取 multipart,每个 part 会产生事件,文件大到需要拆 buffer 时会产生多个事件。
如果你的需求是“边上传边处理、不中转磁盘、还要做实时计算/转码/哈希”,WebFlux 这条路会更舒服(但工程复杂度也会跟着上来)。
3. 配置上传大小限制与临时目录:别让默认值悄悄坑你
3.1 Spring Boot 默认限制是多少?
Spring Boot 的 MultipartProperties(官方 API 文档)把默认值写得明明白白:
max-file-size:单文件最大,默认 1MBmax-request-size:整个请求最大,默认 10MBfile-size-threshold:超过阈值写磁盘,默认 0(也就是几乎都落盘/容器策略相关)location:临时目录;没指定就用系统临时目录
看到没?很多人一上来就喊“为啥我传 5MB 报错”,其实不是你代码不行,是默认就不让你传🙂。
3.2 推荐的 application.yml 配置模板
spring:
servlet:
multipart:
max-file-size: 200MB
max-request-size: 220MB
file-size-threshold: 2MB
location: /data/tmp/uploads
这些属性的含义与默认值,官方来源就是 MultipartProperties(它最终会用于配置 MultipartConfigElement,并且支持 DataSize)。
如果你更喜欢代码配置,也可以用 MultipartConfigFactory 去设置 location、阈值、最大值等(官方 API 里也列了相关 setter)。
我个人(带点情绪)建议:
location尽量配成明确的绝对路径,别让容器默认临时目录“随缘漂移”- 临时目录要监控磁盘容量,否则一旦堆积就很刺激(刺激到你半夜被电话叫醒那种)
3.3 Multipart 解析器:为什么有时候“同样配置,行为不一样”?
Spring MVC 的 multipart 处理是通过 MultipartResolver 策略接口完成的,Servlet 环境下常见实现是 StandardServletMultipartResolver,它使用容器自带的 multipart 解析能力,所以可能会暴露“容器实现差异”。
说人话:你换了 Tomcat/Jetty/Undertow,或者容器版本变了,某些细节可能也会变。
所以大型系统里,上传链路通常会做更多“自我保护”:统一网关限制、后端再限制、对象存储接管大文件。
4. 集成 MinIO 或 AWS S3:别把服务器当硬盘,把它当中转站就好
本节我会给两套“流式接入”示例:
- MinIO(自建/私有云常用)
- AWS S3(公有云标准答案)
核心原则:服务端尽量不落本机大文件,而是把流直接写入对象存储。
4.1 MinIO:用 Java SDK 直接 putObject(流式)
MinIO Java SDK 是一个 S3 客户端,可以对任意 S3 兼容对象存储做 bucket/object 操作。
上传对象的参数类是 PutObjectArgs(官方 Javadoc)。
4.1.1 依赖与客户端构建(示意)
(依赖版本按你们项目锁定即可)
MinioClient minioClient = MinioClient.builder()
.endpoint("https://minio.example.com")
.credentials("ACCESS_KEY", "SECRET_KEY")
.build();
4.1.2 MultipartFile ➜ MinIO(流式直传)
@PostMapping(value = "/upload-to-minio", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Map<String, Object> uploadToMinio(@RequestParam("file") MultipartFile file) throws Exception {
if (file.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "空文件我可不收哈🙂");
}
String objectName = UUID.randomUUID() + "_" + Paths.get(file.getOriginalFilename()).getFileName();
try (InputStream in = file.getInputStream()) {
minioClient.putObject(
PutObjectArgs.builder()
.bucket("demo-bucket")
.object(objectName)
// 关键点:stream + objectSize + partSize(大文件建议分段)
.stream(in, file.getSize(), -1)
.contentType(Optional.ofNullable(file.getContentType())
.orElse("application/octet-stream"))
.build()
);
}
return Map.of("bucket", "demo-bucket", "object", objectName, "size", file.getSize());
}
注:
PutObjectArgs是MinioClient.putObject(PutObjectArgs)的参数类,Javadoc 中明确了它的定位与关联关系。
分段/对象大小等字段也在其继承层次与字段列表里体现(objectSize/partSize/partCount 等)。
4.2.1 同步上传(知道长度就老老实实给长度)
public void uploadToS3(S3Client s3Client, String bucket, String key, InputStream in, long size) {
s3Client.putObject(
b -> b.bucket(bucket).key(key),
RequestBody.fromInputStream(in, size)
);
}
AWS 还非常严肃地警告:长度不匹配会导致截断、失败或连接挂起。
这不是“可能”,这是“迟早”。别跟它赌运气。
4.2.2 大文件/目录:直接上 S3 Transfer Manager
当你要传大文件、甚至传目录,AWS 推荐用 S3 Transfer Manager,它能利用分段上传、并行传输、监控进度、暂停/恢复等能力。
说白了:别自己手搓断点续传,官方给的轮子更稳。
5. 最后给你一份“少踩坑清单”(每条都很现实)
5.1 安全:文件名、路径、类型、大小,一个都别放过
getOriginalFilename()不可信,永远 sanitize 或改成 UUID(Spring Javadoc 也提醒过别盲用)。- 校验 MIME Type 只能当参考,真正安全要做“内容嗅探/白名单后缀/病毒扫描”(看业务强度)
- 目录穿越要防:
normalize()+ 确保目标路径在 root 下面
5.2 稳定性:限制、阈值、临时目录必须“显式配置”
- 默认单文件 1MB、请求 10MB,别等用户报错才想起它。
location配到你能监控、能清理、权限正确的目录。
5.3 性能:能流式就流式,能对象存储就对象存储
- MVC:
getInputStream()+transferTo(),不要getBytes()。 - 真要顺序流式 multipart:考虑 WebFlux 的
Flux<PartEvent>。 - 对象存储:S3 用
RequestBody.fromInputStream且长度准确。
结语:文件传输做得好不好,差别不是“能不能用”,而是“出事时你慌不慌”
把文件上传/下载做成“能跑”的 demo 不难;难的是做到:
- 大文件不爆内存
- 并发不把 IO 打穿
- 临时目录不堆成垃圾山
- 对象存储接入后链路可追踪、可回收、可控权限
这些东西听起来不浪漫,但它们决定了你是“写功能的人”,还是“能把系统跑稳的人”。而我更希望你是后者——毕竟谁也不想半夜被报警吵醒,对着监控骂街,对吧😄。
🧧福利赠与你🧧
无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。
最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。
同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。
ps:本文涉及所有源代码,均已上传至Gitee:
https://gitee.com/bugjun01/SpringBoot-demo开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗
🫵 Who am I?
我是 bug菌:
- 热活跃于 CSDN:
https://blog.csdn.net/weixin_43970743| 掘金:https://juejin.cn/user/695333581765240| InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish| 51CTO:https://blog.51cto.com/u_15700751| 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371| 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke| 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles等技术社区; - CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
- 掘金、InfoQ、51CTO 等平台签约及优质作者;
- 全网粉丝累计 30w+。
更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。
- End -
- 点赞
- 收藏
- 关注作者
评论(0)