(适配版本:SpringBoot4.0.6 + Spring Framework6.x + JDK21及以上,旧版Spring5/低JDK不再讨论)

前言

1、技术债务:废弃老旧 commons-fileupload 实现的设计缘由

在 Spring5 及更早的旧版本中,Spring 采用第三方开源组件commons-fileupload实现文件上传,对应实现类CommonsMultipartFile。该组件底层需要依赖操作系统较高的磁盘读写、临时目录创建权限,同时强绑定第三方Jar包,带来了沉重的技术债务

  1. 依赖第三方包导致项目冗余,不同Tomcat、Jetty等Servlet容器适配参差不齐,极易出现容器版本冲突、文件临时目录权限报错;

  2. 底层磁盘临时文件生命周期由第三方组件管控,存在文件残留、IO资源泄漏、安全漏洞等隐性问题;

  3. 老旧实现代码耦合太深,受第三方组件约束,框架无法灵活迭代文件上传底层逻辑。

因此从 Spring6、SpringBoot4.x(JDK21起步) 版本开始,Spring官方彻底砍掉这套基于第三方的老旧实现,依托Servlet3.0原生Part规范从零重构文件上传底层,完全抛弃commons-fileupload,消除历史技术债,全链路基于JDK和Servlet原生API实现,不再维护、兼容旧版CommonsMultipartFile

2、本文版本约束

本文所有内容仅限 SpringBoot4.0.6、Spring Framework6.x、JDK21及更高运行环境;Spring5.x、JDK8/JDK17、依赖commons-fileupload的历史版本不在本文讲解、参考范围内。

一、MultipartFile 实现体系与框架设计者设计思想

1、继承与实现体系梳理

MultipartFileorg.springframework.web.multipart包下顶层接口,继承父接口InputStreamSource,是Spring MVC定义文件上传操作的统一抽象契约;在当前Spring6体系中,全框架仅提供2个官方实现类,无其他第三方实现:

实现类名称

修饰与位置

运行环境

StandardMultipartFile

private私有静态内部类,隶属于StandardMultipartHttpServletRequest

生产环境真实HTTP文件上传

MockMultipartFile

public公开类,位于spring-test测试依赖包

Junit单元测试模拟文件

注:MockMultipartFile为单元测试专用实现,业务开发阶段无需深入学习、使用,下文重点从设计者视角剖析私有实现的设计目的。

2、设计者视角:将StandardMultipartFile私有化锁死的核心用意

官方把生产唯一实现类定义为私有内部类、禁止开发者手动new实例,是经过框架架构权衡后的设计决策,核心4个目的:

  1. 屏蔽底层复杂实现细节,规避人为错误

StandardMultipartFile底层封装了Servlet原生Part对象、Tomcat临时磁盘文件管理、输入输出流资源自动回收、临时文件自动清理等复杂逻辑。如果对外开放实例化,开发者手动new极易出现:临时文件残留磁盘、IO流未关闭造成资源泄漏、Part生命周期错乱等线上故障;交由Spring MVC容器在解析HTTP请求时自动实例化,由框架统一管控资源生命周期。

  1. 隔离实现与抽象,实现开闭原则

设计者把不稳定的底层实现隐藏,只向外暴露稳定的MultipartFile接口契约。未来Spring如需更换底层文件解析方案(例如改用NIO异步文件读写、对接新Servlet规范),仅需修改StandardMultipartFile内部代码,所有业务层Controller代码完全不用改动

  1. 强制统一创建规则,保证请求上下文安全

文件实例绑定当前HTTP请求上下文,只有经过Spring的StandardServletMultipartResolver解析Multipart请求后生成的实例才是合法可用的,手动构造的实例脱离请求上下文,容易出现上下文丢失、参数异常,私有化从语法层面杜绝非法实例创建。

  1. 精简业务开发者心智负担

业务程序员不用关心底层是Tomcat-Part实现、还是其他容器实现,只需要面向接口调用方法即可,符合面向抽象编程的软件工程思想。

3、接口作为访问边界 + 依赖倒置设计思想

  1. 访问边界

我们在Controller中定义MultipartFile file,变量类型永远是顶层接口类型,而非具体实现类类型。Java语法天然做访问隔离:只能调用接口中声明过的所有公开方法,实现类内部自定义的私有属性、私有方法完全无法访问,接口就是官方划定的安全操作边界,超出边界的底层逻辑对开发者不可见。

  1. 依赖倒置(核心设计思想)

高层业务代码(Controller文件上传逻辑)依赖抽象MultipartFile接口,底层具体文件实现StandardMultipartFile也实现这个接口,高层不依赖低层具体实现,二者共同依赖抽象契约,这是依赖倒置原则在Spring源码中典型落地案例,也是我们「只学接口、不学实现类」的根本原因。

小结:设计者通过「隐藏实现+暴露接口」的架构,把变化的底层实现锁住,把稳定的操作规范对外开放,业务开发只需要学习接口方法。

二、MultipartFile 全接口方法详解(全方法无遗漏,表格规范)

方法签名 作用 业务场景
String getName() 获取表单中文件参数的 name(如uploadFile) 区分多个文件参数
@Nullable String getOriginalFilename() 获取客户端原始文件名(如test.png) 保存文件时用原始文件名
@Nullable String getContentType() 获取文件 MIME 类型(如image/png) 文件类型校验(只允许图片等)
boolean isEmpty() 判断文件是否为空(0 字节或未选择) 上传前校验,避免空文件
long getSize() 获取文件大小(字节) 大小限制校验(如不超过 10MB)
InputStream getInputStream() throws IOException 获取文件输入流 流式处理文件(如上传到 OSS)
byte[] getBytes() throws IOException 获取文件字节数组 小文件快速处理(如生成 MD5)
void transferTo(File dest) throws IOException, IllegalStateException 保存文件到本地磁盘 本地文件存储(最常用)
default void transferTo(Path dest) throws IOException, IllegalStateException 保存文件到 Path 路径 Java NIO 风格文件存储

三、前后端实战落地演练(单文件 + 多文件上传,配套前端+后端全代码)

前置知识点:MultipartFile仅能接收Content-Type:multipart/form-data格式请求;该格式除了携带二进制文件,同时支持普通字符串参数、JSON序列化字符串、Map参数、Protobuf二进制数据,所有参数都以独立part表单项存入FormData,是form-data规范原生能力。

3.1 实战1:单文件上传

① 前端代码(两种实现:原生Form表单 + Axios异步FormData)
方式1:原生HTML Form页面(页面同步提交)
<!DOCTYPE html>
<html>
<body>
    <!-- 必须配置enctype="multipart/form-data"、method="post",才能生成multipart/form-data请求 -->
    <form action="/file/uploadSingle" method="post" enctype="multipart/form-data">
        <!-- 普通文本参数:和文件一起随表单提交,后端用@RequestParam接收 -->
        备注信息:<input type="text" name="fileRemark" value="首页轮播图">
        <br>
        <!-- 文件控件:name="uploadFile" 和后端接口入参名保持一致 -->
        <input type="file" name="uploadFile" accept=".jpg,.png,.jpeg">
        <br>
        <button type="submit">提交上传</button>
    </form>
</body>
</html>

方式2:Axios异步提交(前后端分离项目,无页面刷新)
<!DOCTYPE html>
<html>
<body>
    <input type="file" id="fileDom" accept=".jpg,.png">
    <button onclick="upload()">异步上传文件</button>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
        async function upload(){
            let file = document.getElementById("fileDom").files[0];
            // FormData:模拟multipart/form-data表单容器,可追加【文件、普通字符串、JSON串】
            let formData = new FormData();
            // 1、追加文件
            formData.append("uploadFile",file);
            // 2、追加普通字符串参数
            formData.append("fileRemark","轮播封面图片");
            // 3、追加JSON对象(实体/Map转为字符串存入form-data)
            let bannerObj = {sortOrder:1,isActive:true};
            formData.append("bannerJson",JSON.stringify(bannerObj));
            // 4、Protobuf二进制数据:二进制字节直接存入form-data(二进制part)
            // formData.append("protoData",blob);

            // axios自动识别FormData,自动填充请求头Content-Type:multipart/form-data
            await axios.post("/file/uploadSingle",formData)
                .then(res=>console.log("上传结果:",res.data))
        }
    </script>
</body>
</html>
② 后端SpringBoot Controller代码
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;

@RestController
@RequestMapping("/file")
public class FileController {
    /**
     * 单文件上传接口
     * @param uploadFile 接收前端name=uploadFile的文件
     * @param fileRemark 接收普通表单字符串参数
     * @param bannerJson 接收前端序列化后的JSON字符串,后端手动转实体/Map
     */
    @PostMapping("/uploadSingle")
    public Result<String> uploadSingle(
            @RequestParam("uploadFile") MultipartFile uploadFile,
            @RequestParam("fileRemark") String fileRemark,
            @RequestParam(value = "bannerJson",required = false) String bannerJson
    ) throws IOException {
        // 1、空文件校验
        if(uploadFile.isEmpty()){
            return Result.fail("上传文件不能为空");
        }
        // 2、文件大小、类型校验
        long maxSize = 10*1024*1024;
        if(uploadFile.getSize()>maxSize){
            return Result.fail("文件不能超过10MB");
        }
        String contentType = uploadFile.getContentType();
        if(!contentType.startsWith("image/")){
            return Result.fail("仅允许上传图片文件");
        }
        // 3、获取原始文件名,落地保存到磁盘
        String fileName = uploadFile.getOriginalFilename();
        File saveFile = new File("D:/upload/",fileName);
        // transferTo:Spring内置落地方法
        uploadFile.transferTo(saveFile);
        return Result.success("文件保存成功:"+fileName);
    }
}

3.2 实战2:多文件批量上传

① 前端代码
<!DOCTYPE html>
<html>
<body>
    <!-- multiple开启多文件多选,文件name统一同名 -->
    <form action="/file/uploadBatch" method="post" enctype="multipart/form-data">
        <input type="file" name="uploadFile" multiple accept=".png,.jpg">
        <input type="text" name="batchRemark" value="批量轮播图片">
        <button type="submit">批量上传</button>
    </form>
</body>
</html>

Axios异步批量:

async function batchUpload(){
    let files = document.getElementById("fileInput").files;
    let formData = new FormData();
    // 循环把所有文件追加到formData,参数名统一
    for(let i=0;i<files.length;i++){
        formData.append("uploadFile",files[i]);
    }
    formData.append("batchRemark","批量上传图片");
    await axios.post("/file/uploadBatch",formData);
}
② 后端接收代码(MultipartFile数组接收)
@PostMapping("/uploadBatch")
public Result<String> uploadBatch(
        @RequestParam("uploadFile") MultipartFile[] fileArr,
        @RequestParam("batchRemark") String remark
) throws IOException{
    int count = 0;
    for(MultipartFile file : fileArr){
        if(!file.isEmpty()){
            File dest = new File("D:/upload/",file.getOriginalFilename());
            file.transferTo(dest);
            count++;
        }
    }
    return Result.success("成功上传文件数量:"+count);
}

补充拓展:multipart/form-data参数规范说明(官方RFC规范)

按照HTTP multipart/form-data标准规范:一个表单请求由多个独立part片段组成,每个part拥有唯一name,part内容可以是:

  1. 普通文本字符串(常规@RequestParam字符串参数);

  2. 二进制文件流(MultipartFile接收);

  3. 任意二进制数据(Protobuf、字节数组、压缩包二进制,后端可通过@RequestParam+字节数组接收);

因此业务中需要同时传实体对象+文件时,不能直接把JSON对象丢进JSON请求头,正确方案:把对象JSON序列化字符串作为普通参数存入FormData,后端拿到字符串后手动用Jackson反序列化为Java实体。

四、文档总结

  1. SpringBoot4.0.6+JDK21版本彻底清除commons-fileupload历史技术债,仅保留原生Servlet-Part实现StandardMultipartFile+测试MockMultipartFile

  2. 设计者将生产实现私有化,目的是屏蔽底层细节、约束实例创建、依托接口实现开闭原则与依赖倒置;

  3. 业务开发永远面向MultipartFile接口编程,只使用接口定义的全部公开方法,完全不用关心实现类源码与内部逻辑

  4. 文件上传必须使用multipart/form-data格式,FormData除文件外,天然支持普通参数、JSON串、Protobuf二进制等多类型数据混合提交。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐