0%

文件上传

Spring 文件上传机制详解:从请求解析到实战配置

文件上传是 Web 开发中的常见需求,Spring 提供了灵活且强大的文件上传支持,核心通过 MultipartResolver 接口解析 multipart/form-data 格式的请求。从 “请求格式→解析原理→两种实现类对比→实战配置→常见问题” 五个维度,全面解析 Spring 文件上传的底层机制与最佳实践。

文件上传的基础:multipart/form-data 格式

在讨论 Spring 的实现前,需先明确文件上传的 HTTP 协议基础:客户端必须使用 multipart/form-data 格式提交请求,这是由 RFC 1867 定义的专门用于文件上传的 MIME 类型。

格式特点

  • 数据结构:请求体被分割为多个 “部分(Part)”,每个部分对应一个表单字段(普通字段或文件字段);
  • 分隔符:各部分通过一个唯一的 “边界字符串(Boundary)” 分隔,边界字符串在请求头 Content-Type 中指定(如 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123);
  • 支持内容:可同时传输文件(二进制数据)和普通键值对(文本数据),无需 URL 编码。

示例请求(简化版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /upload HTTP/1.1
Host: localhost:8080
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123
Content-Length: 12345

----WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"

张三
----WebKitFormBoundaryABC123
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg

[二进制图片数据]
----WebKitFormBoundaryABC123--

Spring 文件上传核心:MultipartResolver 接口

Spring 通过 MultipartResolver 接口抽象文件上传的解析逻辑,定义了判断请求是否为 multipart 类型以及解析请求的核心方法:

1
2
3
4
5
6
7
8
9
10
public interface MultipartResolver {
// 判断请求是否为 multipart/form-data 格式
boolean isMultipart(HttpServletRequest request);

// 解析 multipart 请求,返回 MultipartHttpServletRequest(封装了文件和普通参数)
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

// 清理解析过程中产生的临时资源(如临时文件)
void cleanupMultipart(MultipartHttpServletRequest request);
}

该接口有两个主要实现类,适用于不同环境:

实现类 依赖技术 适用场景 核心优势
CommonsMultipartResolver Apache Commons FileUpload 组件 所有 Servlet 容器(包括 Servlet 2.5 及以下) 兼容性好,配置灵活(支持在 Spring 中设置上传参数)
StandardServletMultipartResolver Servlet 3.0+ 内置的文件上传支持 支持 Servlet 3.0+ 的容器(如 Tomcat 7+、Jetty 8+) 无需额外依赖,原生支持 Servlet 规范

CommonsMultipartResolver:基于 Commons FileUpload 的实现

CommonsMultipartResolver 是 Spring 早期文件上传的主流实现,依赖 Apache 的 commons-fileupload 组件解析 multipart 请求,适用于所有 Servlet 容器。

1. 依赖配置

需在 pom.xml 中添加 commons-fileupload 依赖:

1
2
3
4
5
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version> <!-- 最新稳定版 -->
</dependency>

2. Spring 配置(两种方式)

(1)XML 配置

在 Spring 配置文件(如 springmvc.xml)中定义 CommonsMultipartResolver 的 Bean:

1
2
3
4
5
6
7
8
9
10
11
12
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 可选:默认编码(匹配前端表单编码) -->
<property name="defaultEncoding" value="UTF-8"/>
<!-- 单个文件最大大小(字节),-1 表示无限制 -->
<property name="maxUploadSizePerFile" value="10485760"/> <!-- 10MB -->
<!-- 整个请求最大大小(字节),包含所有文件和普通参数 -->
<property name="maxUploadSize" value="52428800"/> <!-- 50MB -->
<!-- 内存中缓存文件的阈值(字节),超过则写入临时文件 -->
<property name="maxInMemorySize" value="4096"/> <!-- 4KB -->
<!-- 临时文件存储目录(默认使用 Servlet 容器的临时目录) -->
<property name="uploadTempDir" value="/WEB-INF/tmp"/>
</bean>
(2)Java Config 配置

@Configuration 类中通过 @Bean 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.multipart.MultipartResolver;
import java.io.File;

@Configuration
public class FileUploadConfig {
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver resolver = new CommonsMultipartResolver();
resolver.setDefaultEncoding("UTF-8");
resolver.setMaxUploadSizePerFile(10 * 1024 * 1024); // 10MB
resolver.setMaxUploadSize(50 * 1024 * 1024); // 50MB
resolver.setMaxInMemorySize(4096);
// 设置临时目录(需确保目录存在,否则会报错)
resolver.setUploadTempDir(new FileSystemResource(new File("/WEB-INF/tmp")));
return resolver;
}
}

3. 解析原理

  1. 判断请求类型isMultipart() 方法检查请求头 Content-Type 是否以 multipart/ 开头;
  2. 解析请求体resolveMultipart() 方法使用 commons-fileuploadServletFileUpload 解析请求体,将每个 Part 封装为 CommonsMultipartFile 对象;
  3. 封装请求对象:返回 DefaultMultipartHttpServletRequestMultipartHttpServletRequest 的实现类),提供 getFile()(获取文件)、getParameter()(获取普通参数)等方法;
  4. 清理资源:请求处理完成后,cleanupMultipart() 方法删除临时文件,释放资源。

StandardServletMultipartResolver:基于 Servlet 3.0+ 的实现

Servlet 3.0 规范(JSR 315)内置了文件上传支持,StandardServletMultipartResolver 直接使用该规范,无需额外依赖,是 Servlet 3.0+ 环境的推荐选择。

1. 核心区别:配置位置的变化

CommonsMultipartResolver 不同,StandardServletMultipartResolver上传参数(如文件大小限制)不能在 Spring 配置中设置,必须在 Servlet 配置 中定义(因参数由 Servlet 容器管理)。

2. 配置步骤

(1)配置 Servlet 上传参数

需在 web.xml 中为 DispatcherServlet 添加 <multipart-config> 元素,或通过 Java 代码配置 MultipartConfigElement

方式 1:web.xml 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
<!-- Servlet 3.0+ 文件上传配置 -->
<multipart-config>
<!-- 临时文件存储目录(默认使用 Servlet 容器的临时目录) -->
<location>/tmp</location>
<!-- 单个文件最大大小(字节),-1 表示无限制 -->
<max-file-size>10485760</max-file-size> <!-- 10MB -->
<!-- 整个请求最大大小(字节) -->
<max-request-size>52428800</max-request-size> <!-- 50MB -->
<!-- 内存中缓存文件的阈值(字节) -->
<file-size-threshold>4096</file-size-threshold> <!-- 4KB -->
</multipart-config>
<load-on-startup>1</load-on-startup>
</servlet>
方式 2:Java 代码配置(Servlet 3.0+ 无 web.xml 场景)

若使用 AbstractAnnotationConfigDispatcherServletInitializer 初始化 Spring MVC,可通过 getServletRegistration() 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletRegistration;
import java.io.File;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}

@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}

// 配置文件上传参数
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// 临时目录(需确保存在)
String tempDir = System.getProperty("java.io.tmpdir");
MultipartConfigElement multipartConfig = new MultipartConfigElement(
tempDir, // location
10 * 1024 * 1024, // maxFileSize(10MB)
50 * 1024 * 1024, // maxRequestSize(50MB)
4096 // fileSizeThreshold(4KB)
);
registration.setMultipartConfig(multipartConfig);
}
}
(2)配置 StandardServletMultipartResolver

在 Spring 配置中定义 StandardServletMultipartResolver 的 Bean(无需设置上传参数):

XML 配置
1
2
3
4
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver">
<!-- 可选:默认编码 -->
<property name="defaultEncoding" value="UTF-8"/>
</bean>
Java Config 配置
1
2
3
4
5
6
7
8
9
@Configuration
public class FileUploadConfig {
@Bean
public MultipartResolver multipartResolver() {
StandardServletMultipartResolver resolver = new StandardServletMultipartResolver();
resolver.setDefaultEncoding("UTF-8");
return resolver;
}
}

3. 解析原理

  1. 判断请求类型:与 CommonsMultipartResolver 一致,检查 Content-Type 是否为 multipart/ 类型;
  2. 委托 Servlet 容器解析resolveMultipart() 方法调用 Servlet 容器的 request.getParts() 方法,获取所有 Part(依赖 Servlet 3.0+ 的 HttpServletRequest 扩展);
  3. 封装文件对象:将 Servlet 的 Part 封装为 StandardMultipartFileMultipartFile 的实现类);
  4. 清理资源:同样通过 cleanupMultipart() 方法清理临时文件,但实际由 Servlet 容器管理临时文件的生命周期。

Controller 接收文件的两种方式

无论使用哪种 MultipartResolver,Controller 中接收文件的方式一致,主要通过 @RequestPart@RequestParam 注解结合 MultipartFile 接口实现。

1. 使用 MultipartFile 接收单个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;

@RestController
public class FileUploadController {

// 前端表单:<input type="file" name="avatar" accept="image/*">
@PostMapping(value = "/upload/single", consumes = "multipart/form-data")
public String uploadSingleFile(
@RequestPart("avatar") MultipartFile file, // 匹配表单字段名 "avatar"
@RequestPart("username") String username) { // 同时接收普通参数

// 检查文件是否为空
if (file.isEmpty()) {
return "上传失败:文件为空";
}

try {
// 获取文件名(原始文件名,可能包含路径,需处理)
String originalFilename = file.getOriginalFilename();
// 提取文件名(避免客户端路径影响)
String filename = originalFilename.substring(originalFilename.lastIndexOf(File.separator) + 1);
// 保存文件到本地目录(实际项目中建议使用分布式文件系统,如 MinIO)
File dest = new File("D:/uploads/" + filename);
// 确保父目录存在
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
// 转存文件
file.transferTo(dest);
return "上传成功:" + username + " 的文件 " + filename + "(大小:" + file.getSize() + "字节)";
} catch (IOException e) {
return "上传失败:" + e.getMessage();
}
}
}

2. 使用 List<MultipartFile> 接收多个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;

@RestController
public class MultiFileUploadController {

// 前端表单:<input type="file" name="files" multiple accept="image/*">
@PostMapping("/upload/multiple")
public String uploadMultipleFiles(@RequestPart("files") List<MultipartFile> files) {
int successCount = 0;
for (MultipartFile file : files) {
if (!file.isEmpty()) {
try {
// 保存文件(逻辑同上)
String filename = file.getOriginalFilename();
file.transferTo(new File("D:/uploads/" + filename));
successCount++;
} catch (IOException e) {
// 忽略单个文件失败,继续处理其他文件
}
}
}
return "上传完成:成功 " + successCount + " 个,共 " + files.size() + " 个文件";
}
}

3. @RequestPart vs @RequestParam

两者均可接收文件,但存在细微区别:

  • @RequestPart:更适合混合传输文件和复杂数据(如 JSON 字符串),支持通过 HttpMessageConverter 解析非文件 Part(如 JSON 转对象);
  • @RequestParam:传统用法,更适合仅传输文件和简单键值对,默认将文件视为 “请求参数”。

实际开发中,推荐使用 @RequestPart,语义更清晰,支持场景更丰富。

常见问题与解决方案

1. 上传文件时报 The current request is not a multipart request

  • 原因:前端请求未使用 multipart/form-data 格式,或 MultipartResolver 配置错误(如 Bean ID 非 multipartResolver);
  • 解决方案:
    • 前端表单添加 enctype="multipart/form-data"(如 <form enctype="multipart/form-data" method="post">);
    • 确保 Spring 配置中 MultipartResolver 的 Bean ID 为 multipartResolver(Spring 会按此 ID 自动查找)。

2. 文件大小超过限制时无响应或报 500 错误

  • 原因:未处理 MaxUploadSizeExceededException 异常;

  • 解决方案:通过@ControllerAdvice全局捕获异常:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.multipart.MaxUploadSizeExceededException;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

    @ControllerAdvice
    public class GlobalExceptionHandler {
    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public String handleMaxUploadSizeException(MaxUploadSizeExceededException e) {
    return "上传失败:文件大小超过限制(最大 " + e.getMaxUploadSize() + " 字节)";
    }
    }

3. 临时文件清理问题

  • 原因MultipartResolver 未正常调用 cleanupMultipart() 方法,导致临时文件堆积;
  • 解决方案:
    • 确保 Spring MVC 版本 ≥ 3.2(自动在请求完成后清理);
    • 手动调用 multipartResolver.cleanupMultipart(request)(适用于特殊场景)。

4. 中文文件名乱码

  • 原因MultipartResolver 的默认编码与前端不一致;
  • 解决方案:在 MultipartResolver 配置中显式设置 defaultEncodingUTF-8(如上文配置示例)。

两种实现类的选择建议

场景 推荐实现类 决策依据
Servlet 容器版本 < 3.0(如 Tomcat 6) CommonsMultipartResolver 无 Servlet 3.0 支持,必须依赖第三方组件
Servlet 容器版本 ≥ 3.0 StandardServletMultipartResolver 无需额外依赖,遵循 Servlet 规范,配置更统一
需要在 Spring 中集中管理上传参数 CommonsMultipartResolver 支持在 Spring 配置中设置所有参数
追求轻量级、无第三方依赖 StandardServletMultipartResolver 仅依赖 Servlet 容器,减少 Jar 包冲突风险

总结

Spring 文件上传的核心是 MultipartResolver 接口,通过 CommonsMultipartResolverStandardServletMultipartResolver 两种实现类,分别适配不同的 Servlet 环境。关键要点:

  1. 请求格式:必须使用 multipart/form-data 格式;
  2. 配置区别CommonsMultipartResolver 在 Spring 中配置上传参数,StandardServletMultipartResolver 在 Servlet 中配置;
  3. 接收方式:通过 @RequestPart 结合 MultipartFile 接口接收文件,支持单个 / 多个文件;
  4. 异常处理:需全局捕获 MaxUploadSizeExceededException 等上传相关异常

欢迎关注我的其它发布渠道