你以为文件上传就是收个 MultipartFile?那你敢不敢让用户传 2GB 试试?

举报
bug菌 发表于 2026/01/13 16:44:59 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学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:单文件最大,默认 1MB
  • max-request-size:整个请求最大,默认 10MB
  • file-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());
}

注:PutObjectArgsMinioClient.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 -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。