文件上传自动配置
概述
Spring Boot 通过 MultipartAutoConfiguration 自动配置文件上传功能,核心是 StandardServletMultipartResolver 和 MultipartConfigElement。文件上传涉及从请求解析、类型转换到文件存储的完整链路。
本文将深入拆解 Spring Boot 文件上传自动配置的 8 个细节点。
本文基于 Spring Boot 3.x + Servlet 3.0+ 源码分析。
1. MultipartAutoConfiguration 的 @ConditionalOnMissingBean
1.1 源码
java
// MultipartAutoConfiguration.java
@AutoConfiguration
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnMissingBean({ MultipartConfigElement.class,
StandardServletMultipartResolver.class, CommonsMultipartResolver.class })
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {
private final MultipartProperties multipartProperties;
public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}
// 1. 创建 MultipartConfigElement
@Bean
@ConditionalOnMissingBean
MultipartConfigElement multipartConfigElement() {
// 从 MultipartProperties 读取配置
// 包含 maxFileSize, maxRequestSize, fileSizeThreshold, location
return this.multipartProperties.createMultipartConfig();
}
// 2. 创建 StandardServletMultipartResolver
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
StandardServletMultipartResolver multipartResolver() {
// 根据 resolveLazily 配置决定懒加载行为
StandardServletMultipartResolver resolver =
new StandardServletMultipartResolver();
resolver.setResolveLazily(
this.multipartProperties.getResolveLazily());
return resolver;
}
}1.2 @ConditionalOnMissingBean 三个条件的含义
java
// 条件 1: @ConditionalOnMissingBean(MultipartConfigElement.class)
// 用户自定义了 MultipartConfigElement Bean → 跳过自动配置
//
// 条件 2: @ConditionalOnMissingBean(StandardServletMultipartResolver.class)
// 用户自定义了 StandardServletMultipartResolver Bean → 跳过自动配置
//
// 条件 3: @ConditionalOnMissingBean(CommonsMultipartResolver.class)
// 用户自定义了 CommonsMultipartResolver Bean → 跳过自动配置
// ★ 特别注意: CommonsMultipartResolver 是 Apache Commons FileUpload 的实现
// 在 Spring Boot 3.x 中已标记为废弃,建议使用 Servlet 3.0+ 标准实现
//
// 任一条件满足 → MultipartAutoConfiguration 不生效1.3 三个条件的判断逻辑
java
// @ConditionalOnMissingBean 的 SearchStrategy
// 默认搜索策略: SearchStrategy.ALL
// 意味着会检查当前容器及其所有父容器
// 例如: 如果用户在 @Configuration 中定义了:
@Configuration
public class CustomMultipartConfig {
@Bean
MultipartConfigElement customMultipartConfig() {
// 用户自定义的配置
return new MultipartConfigElement("/tmp/custom-path");
}
@Bean
StandardServletMultipartResolver customResolver() {
// 用户自定义的解析器
return new StandardServletMultipartResolver();
}
}
// → MultipartAutoConfiguration 检测到已存在
// MultipartConfigElement 和 MultipartResolver Bean
// → 整个自动配置类不生效
// → @Bean multipartConfigElement() 和 @Bean multipartResolver() 都不会创建1.4 为什么同时检查三个类
java
// MultipartConfigElement 和 MultipartResolver(StandardServletMultipartResolver)
// 二者必须同时存在才能正常工作
// MultipartConfigElement:
// → 配置 Servlet 3.0+ 的 @MultipartConfig 注解
// → 控制文件大小限制、存储路径等
// StandardServletMultipartResolver:
// → 实现 Spring MVC 的 MultipartResolver 接口
// → 在 DispatcherServlet 中用于解析 multipart 请求
// 缺少任何一个 → 文件上传功能不完整
// CommonsMultipartResolver 检查:
// → 用户可能选择 Apache Commons FileUpload 而非 Servlet 标准
// → 两者互斥,不能同时生效2. MultipartProperties 的 5 个核心属性
2.1 源码
java
// MultipartProperties.java
@ConfigurationProperties(prefix = "spring.servlet.multipart")
public class MultipartProperties {
// 1. maxFileSize — 单个文件最大大小
// 默认: 1MB
// 格式: "10MB"、"1GB"、"512KB"
private DataSize maxFileSize = DataSize.ofMegabytes(1);
// 2. maxRequestSize — 整个 multipart 请求的最大大小
// 默认: 10MB
// 示例: "10MB"、"100MB"
private DataSize maxRequestSize = DataSize.ofMegabytes(10);
// 3. fileSizeThreshold — 文件大小阈值
// 超过此大小 → 写入磁盘临时文件
// 小于此大小 → 保存在内存中
// 默认: 0(全部写入磁盘)
// 建议: 设置为 256KB 或 512KB,小文件直接从内存读取
private DataSize fileSizeThreshold = DataSize.ofBytes(0);
// 4. location — 临时文件存储目录
// 默认: ""(使用 Servlet 容器的默认临时目录)
// Tomcat 默认: ${java.io.tmpdir}/tomcat.${port}.${random}
// 可自定义: "/data/upload-tmp"
private String location = "";
// 5. resolveLazily — 是否延迟解析文件
// false(默认): 请求到达时立即解析
// true: 只有访问文件时才解析(减少不必要的开销)
private boolean resolveLazily = false;
}2.2 配置示例
yaml
# 默认配置
spring:
servlet:
multipart:
max-file-size: 1MB # 单文件最大 1MB
max-request-size: 10MB # 请求最大 10MB
file-size-threshold: 0 # 所有文件都写入磁盘
location: "" # 使用容器临时目录
resolve-lazily: false # 立即解析
# 大文件上传配置
spring:
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 200MB
file-size-threshold: 512KB # 512KB 以内的文件在内存中
location: /data/upload-tmp # 指定临时目录
# 禁用文件上传
spring:
servlet:
multipart:
enabled: false # 完全禁用 multipart 请求解析3. fileSizeThreshold 的字节单位处理
3.1 源码
java
// MultipartProperties.createMultipartConfig()
public MultipartConfigElement createMultipartConfig() {
// 将配置转换为 MultipartConfigElement
// 内部使用 DataSize 进行单位转换
// 1. fileSizeThreshold 转换为 int(单位: bytes)
// DataSize.ofMegabytes(10) → 10 * 1024 * 1024 = 10485760
// DataSize.ofKilobytes(512) → 512 * 1024 = 524288
// DataSize.ofBytes(0) → 0
int fileSizeThreshold = (int) this.fileSizeThreshold.toBytes();
// 2. maxFileSize 转换为 long(单位: bytes)
long maxFileSize = this.maxFileSize.toBytes();
// 3. maxRequestSize 转换为 long(单位: bytes)
long maxRequestSize = this.maxRequestSize.toBytes();
// 4. location 存储路径
String location = this.location;
// 创建 MultipartConfigElement
return new MultipartConfigElement(
location, // 临时存储目录
maxFileSize, // 单文件最大大小(bytes)
maxRequestSize, // 请求最大大小(bytes)
fileSizeThreshold // 内存阈值(bytes)
);
}3.2 DataSize 的单位解析
java
// DataSize 是 Spring Boot 提供的专门处理字节大小的类
// 支持多种单位格式:
// 支持的字符串格式:
DataSize.parse("1B") // 1 byte
DataSize.parse("1KB") // 1024 bytes(1024^1)
DataSize.parse("1MB") // 1,048,576 bytes(1024^2)
DataSize.parse("1GB") // 1,073,741,824 bytes(1024^3)
DataSize.parse("1TB") // 1,099,511,627,776 bytes(1024^4)
// 简便方法:
DataSize.ofBytes(1024) // 1024 bytes
DataSize.ofKilobytes(1) // 1024 bytes
DataSize.ofMegabytes(10) // 10,485,760 bytes
DataSize.ofGigabytes(1) // 1,073,741,824 bytes
// 在 yml 中的写法:
spring.servlet.multipart.file-size-threshold: 256KB
spring.servlet.multipart.max-file-size: 10MB
spring.servlet.multipart.max-request-size: 100MB3.3 阈值对内存和磁盘的影响
yaml
# fileSizeThreshold = 0(默认)
# → 所有文件都写入磁盘
# → 优点: 节省内存
# → 缺点: 小文件也需要磁盘 IO
# fileSizeThreshold = 256KB
# → 小于 256KB 的文件保存在内存中
# → 大于 256KB 的文件写入磁盘
# → 优点: 小文件处理更快
# → 缺点: 同时上传大量小文件可能 OOM
# fileSizeThreshold = 10MB
# → 小于 10MB 的文件保存在内存中
# → 优点: 绝大多数文件直接读内存
# → 缺点: 内存占用高,适合高并发小文件场景4. MultipartConfigElement 创建
4.1 MultipartConfigElement 的四个参数
java
// MultipartConfigElement.java (Servlet API)
public class MultipartConfigElement {
// 构造函数参数:
// 1. location — 临时文件存储路径
// 2. maxFileSize — 单文件最大大小(-1 表示不限制)
// 3. maxRequestSize — 请求最大大小(-1 表示不限制)
// 4. fileSizeThreshold — 内存阈值
public MultipartConfigElement(
String location, // 临时目录
long maxFileSize, // 单文件上限
long maxRequestSize, // 请求上限
int fileSizeThreshold // 内存阈值
) { ... }
}4.2 @MultipartConfig 注解在 Tomcat 中的注册
java
// Servlet 3.0+ 通过 @MultipartConfig 注解启用文件上传
// Spring Boot 不直接在类上使用注解,而是通过编程方式注册
// Spring Boot 的方式:
// 1. MultipartAutoConfiguration 创建 MultipartConfigElement Bean
//
// 2. 在准备 Servlet 容器时(TomcatServletWebServerFactory):
// Tomcat 的 Context 会查找 MultipartConfigElement
//
// 3. 当注册 DispatcherServlet 时:
// ServletRegistrationBean 在 addRegistration() 时
// 将 MultipartConfigElement 设置到 Servlet 的注册信息中
// 在 Tomcat 中的注册代码(简化):
// TomcatServletWebServerFactory.java
public void configureTomcat(Context context) {
// 查找 MultipartConfigElement Bean
MultipartConfigElement multipartConfig =
context.getBeanFactory()
.getBean(MultipartConfigElement.class);
// 获取 DispatcherServlet 的 Wrapper
Wrapper wrapper = (Wrapper) context.findChild("dispatcherServlet");
if (wrapper != null && multipartConfig != null) {
// 设置 @MultipartConfig 配置
wrapper.setMultipartConfigElement(multipartConfig);
}
}4.3 MultipartConfigElement 在 Tomcat 中对应的配置
java
// 在 web.xml 中相当于:
// <servlet>
// <servlet-name>dispatcherServlet</servlet-name>
// <servlet-class>DispatcherServlet</servlet-class>
// <multipart-config>
// <location>/tmp</location>
// <max-file-size>1048576</max-file-size>
// <max-request-size>10485760</max-request-size>
// <file-size-threshold>0</file-size-threshold>
// </multipart-config>
// </servlet>
// 在 Servlet 3.0+ 注解中相当于:
// @WebServlet(name = "dispatcherServlet", urlPatterns = "/")
// @MultipartConfig(
// location = "/tmp",
// maxFileSize = 1048576, // 1MB
// maxRequestSize = 10485760, // 10MB
// fileSizeThreshold = 0
// )5. StandardServletMultipartResolver.parseRequest() 时机
5.1 源码
java
// StandardServletMultipartResolver.java
public class StandardServletMultipartResolver implements MultipartResolver {
private boolean resolveLazily = false;
@Override
public boolean isMultipart(HttpServletRequest request) {
// 检查请求是否包含 multipart 内容
// 条件: Content-Type 以 "multipart/" 开头
// 如: Content-Type: multipart/form-data; boundary=----WebKitFormBoundary
return MultipartUtils.isMultipartContent(request);
}
@Override
public MultipartHttpServletRequest resolveMultipart(
HttpServletRequest request) throws MultipartException {
// 1. 检查请求是否是 multipart
if (!isMultipart(request)) {
throw new MultipartException("Not a multipart request");
}
// 2. 将请求包装为 StandardMultipartHttpServletRequest
// 此时不会立即解析,而是创建一个包装类
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
@Override
public void cleanupMultipart(
MultipartHttpServletRequest request) {
// 清理临时文件
// 在请求处理完成后由 DispatcherServlet 调用
if (request != null) {
try {
// 删除所有上传的临时文件
request.getParts().forEach(part -> {
try {
part.delete();
} catch (IOException ex) {
// 忽略删除失败
}
});
} catch (Exception ex) {
// 忽略
}
}
}
}5.2 resolveMultipart() 在 DispatcherServlet 中的调用
java
// DispatcherServlet.java
protected HttpServletRequest checkMultipart(HttpServletRequest request)
throws MultipartException {
// 1. 检查请求是否是 multipart
if (this.multipartResolver != null &&
this.multipartResolver.isMultipart(request)) {
// 2. 如果是 multipart → 使用 MultipartResolver 解析
// 这里调用 resolveMultipart(),创建包装请求
return this.multipartResolver.resolveMultipart(request);
}
// 3. 如果不是 multipart → 直接返回原请求
return request;
}
// 调用时机:
// DispatcherServlet.doDispatch() → doService() → checkMultipart()
// 也就是说,在所有 Handler 处理之前,先判断并使用 MultipartResolver 处理请求
//
// 调用链:
// doDispatch(HttpServletRequest request, HttpServletResponse response)
// → checkedRequest = checkMultipart(request)
// → HandlerExecutionChain mappedHandler = getHandler(checkedRequest)
// → HandlerAdapter handle(checkedRequest, ...)
// → finally: cleanupMultipart(checkedRequest)5.3 request.getParts() 首次调用的时机
java
// Servlet 3.0+ 的 multipart 解析在 request.getParts() 首次调用时触发
// 具体流程:
// 1. StandardServletMultipartHttpServletRequest.getParts()
// → 检查缓存,如果已解析则直接返回
// → 如果未解析,调用 parseRequest()
// 2. parseRequest() 方法:
// → request.getParts() 首次调用
// → Tomcat 的 Request.parseParts() 被触发
// → 检查 @MultipartConfig 配置
// → 读取请求体
// → 按 boundary 拆分每一部分
// → 检查 maxFileSize / maxRequestSize
// → 根据 fileSizeThreshold 决定存入内存还是磁盘
// 3. Tomcat 内部的文件大小检查:
// 如果文件大小超过 maxFileSize → IllegalStateException
// 如果请求大小超过 maxRequestSize → IllegalStateException6. StandardMultipartHttpServletRequest 懒加载
6.1 源码
java
// StandardMultipartHttpServletRequest.java
public class StandardMultipartHttpServletRequest
extends HttpServletRequestWrapper implements MultipartHttpServletRequest {
private final boolean resolveLazily;
// 懒加载的 Map,存储解析后的文件
private MultiValueMap<String, MultipartFile> multipartFiles;
// 懒加载的 Map,存储解析后的请求参数
private MultiValueMap<String, String> multipartParameters;
// 解析锁
private final Object lock = new Object();
public StandardMultipartHttpServletRequest(
HttpServletRequest request, boolean resolveLazily) {
super(request);
this.resolveLazily = resolveLazily;
if (!resolveLazily) {
// 如果未开启懒加载 → 立即解析
parseRequest(request);
}
// 如果开启了懒加载 → 等待首次访问时解析
}
@Override
public MultipartFile getFile(String name) {
// 1. 检查 multipartFiles 是否已解析
if (this.multipartFiles == null) {
synchronized (this.lock) {
if (this.multipartFiles == null) {
// ★ 触发懒加载 ★
parseRequest((HttpServletRequest) getRequest());
}
}
}
// 2. 从已解析的文件中获取
return this.multipartFiles.getFirst(name);
}
@Override
public List<MultipartFile> getFiles(String name) {
// 同上,触发懒加载
if (this.multipartFiles == null) {
synchronized (this.lock) {
if (this.multipartFiles == null) {
parseRequest((HttpServletRequest) getRequest());
}
}
}
return this.multipartFiles.get(name);
}
@Override
public String getParameter(String name) {
// 注意:getParameter() 也可能触发 multipart 参数解析
// multipart/form-data 请求中的字段也会作为请求参数
if (this.multipartParameters == null) {
synchronized (this.lock) {
if (this.multipartParameters == null) {
parseRequest((HttpServletRequest) getRequest());
}
}
}
return super.getParameter(name);
}
}6.2 parseRequest() 的实现
java
// StandardMultipartHttpServletRequest.java
private void parseRequest(HttpServletRequest request) {
try {
// 1. 获取 Servlet 3.0+ 的原生 Part 对象
// 首次调用 request.getParts() → Tomcat 解析 multipart 请求
Collection<Part> parts = request.getParts();
// 2. 将 Part 转换为 Spring 的 MultipartFile
this.multipartFiles = new LinkedMultiValueMap<>();
this.multipartParameters = new LinkedMultiValueMap<>();
for (Part part : parts) {
// 获取表单字段名(Content-Disposition 中的 name)
String filename = getFileName(part);
if (filename != null) {
// a) 如果 Part 包含文件名 → 是文件
StandardMultipartFile file = new StandardMultipartFile(
part, filename);
this.multipartFiles.add(part.getName(), file);
} else {
// b) 如果 Part 不包含文件名 → 是普通表单字段
// 将 Part 的内容作为字符串读取
String value = part.getInputStream()...;
this.multipartParameters.add(part.getName(), value);
}
}
} catch (Exception ex) {
throw new MultipartException(
"Could not parse multipart request", ex);
}
}6.3 懒加载与非懒加载的对比
java
// 场景: 客户端上传一个文件和一个普通字段
// POST /upload
// Content-Type: multipart/form-data; boundary=----xxx
// ------xxx
// Content-Disposition: form-data; name="file"; filename="photo.jpg"
// Content-Type: image/jpeg
//
// [二进制数据]
// ------xxx
// Content-Disposition: form-data; name="description"
//
// 一张风景照
// ------xxx--
// resolveLazily = false(立即解析)
// 1. DispatcherServlet.checkMultipart() 调用 multipartResolver.resolveMultipart()
// 2. StandardServletMultipartResolver 创建 StandardMultipartHttpServletRequest
// 3. 构造函数中立即调用 parseRequest()
// 4. parseRequest() 内部调用 request.getParts()
// 5. Tomcat 立即从请求体解析文件并写入磁盘
// 6. MultipartFile 对象已准备好
// → 控制器可以直接使用 @RequestParam MultipartFile file
// resolveLazily = true(延迟解析)
// 1. DispatcherServlet.checkMultipart() 调用 multipartResolver.resolveMultipart()
// 2. StandardServletMultipartResolver 创建 StandardMultipartHttpServletRequest
// 3. 构造函数中不解析,只创建包装类
// 4. 直到控制器中实际访问 MultipartFile 时才解析
// → 如果控制器没有接收文件参数,可以完全避免解析开销7. MultipartResolutionDelegate 解析 4 种类型
7.1 源码
java
// MultipartResolutionDelegate.java
public abstract class MultipartResolutionDelegate {
// 解析的 4 种类型:
// 1. MultipartFile — 单个文件
// 2. Part — Servlet 原生 Part
// 3. MultipartFile[] — 多文件(数组)
// 4. Part[] / Collection<Part> — 原生 Part 集合/数组
// 在 HandlerMethodArgumentResolver (如 RequestParamMethodArgumentResolver) 中
// 通过 MultipartResolutionDelegate 判断是 multipart 类型
// 判断是否是 multipart 类型
public static boolean isMultipartType(Class<?> type) {
return (MultipartFile.class == type || // MultipartFile
Part.class == type || // Part
MultipartFile[].class == type || // MultipartFile[]
Part[].class == type); // Part[]
}
// 解析 multipart 参数
@Nullable
public static Object resolveMultipartArgument(
String name, MethodParameter parameter,
HttpServletRequest request) throws Exception {
// 获取 MultipartHttpServletRequest
MultipartHttpServletRequest multipartRequest =
RequestContextUtils.getMultipartRequest(request);
if (multipartRequest == null) {
return null;
}
Class<?> paramType = parameter.getNestedParameterType();
// 1. MultipartFile(单个文件)
if (MultipartFile.class == paramType) {
return multipartRequest.getFile(name);
}
// 2. Part(Servlet 原生)
if (Part.class == paramType) {
// 获取 Servlet 原生 Part
List<Part> parts = new ArrayList<>(
multipartRequest.getParts(name));
return parts.isEmpty() ? null : parts.get(0);
}
// 3. MultipartFile[] / Collection<MultipartFile>(多文件)
if (MultipartFile[].class == paramType ||
CollectionUtils.isMultiValueMapType(paramType)) {
List<MultipartFile> files = multipartRequest.getFiles(name);
return files.isEmpty() ? null :
files.toArray(new MultipartFile[0]);
}
// 4. Part[] / Collection<Part>(原生 Part 集合)
if (Part[].class == paramType ||
CollectionUtils.isMultiValueMapType(paramType)) {
Collection<Part> parts = multipartRequest.getParts(name);
return parts.isEmpty() ? null :
parts.toArray(new Part[0]);
}
return null; // 不是 multipart 类型
}
}7.2 控制器的 4 种接收方式
java
// 控制器中接收文件上传的 4 种方式
// 方式 1: MultipartFile(单个文件)
@PostMapping("/upload")
public String uploadFile(@RequestParam("file") MultipartFile file) {
// file.getOriginalFilename() → "photo.jpg"
// file.getSize() → 102400
// file.getInputStream() → 文件流
// file.getBytes() → 字节数组
// file.transferTo(new File("/data/photo.jpg"))
return "OK";
}
// 方式 2: Part(Servlet 原生)
@PostMapping("/upload-part")
public String uploadPart(@RequestParam("file") Part part) {
// part.getSubmittedFileName() → "photo.jpg"
// part.getSize() → 102400
// part.getInputStream() → 文件流
// part.write("/data/photo.jpg")
return "OK";
}
// 方式 3: MultipartFile[](多文件,数组)
@PostMapping("/upload-multiple")
public String uploadMultiple(
@RequestParam("files") MultipartFile[] files) {
for (MultipartFile file : files) {
// 处理每个文件
}
return "OK";
}
// 方式 4: Part[](原生 Part 集合)
@PostMapping("/upload-parts")
public String uploadParts(@RequestParam("parts") Part[] parts) {
for (Part part : parts) {
// 处理每个 Part
}
return "OK";
}7.3 MultipartFile 接口的方法
java
// MultipartFile.java (Spring 接口)
public interface MultipartFile {
// 获取文件名(客户端原始文件名)
String getOriginalFilename();
// 获取文件内容类型
String getContentType();
// 文件是否为空
boolean isEmpty();
// 文件大小(字节)
long getSize();
// 获取输入流
InputStream getInputStream() throws IOException;
// 获取字节数组
byte[] getBytes() throws IOException;
// 将文件保存到目标路径
void transferTo(File dest) throws IOException;
// 将文件保存到目标路径(NIO 版本)
void transferTo(Path dest) throws IOException;
}
// StandardMultipartFile 的实现:
// 底层通过 Part.write() 方法保存文件
// Part.write("/data/upload.jpg") → Tomcat 内部将临时文件复制到目标路径8. resolveLazily=true 的效果
8.1 执行流程对比
resolveLazily = false(默认):
┌─────────────────────────────────────────┐
│ 请求到达 │
│ │
│ 1. DispatcherServlet.checkMultipart() │
│ │
│ 2. StandardServletMultipartResolver │
│ .resolveMultipart(request) │
│ └─ new StandardMultipartHttpServletRequest(request, false)
│ └─ 构造函数中立即调用 parseRequest()
│ └─ request.getParts() │ ← 立即解析,读取请求体
│ │
│ 3. 控制器执行 │
│ public String upload( │
│ @RequestParam MultipartFile f) { │
│ // 文件已可用 │
│ } │
└─────────────────────────────────────────┘
resolveLazily = true:
┌─────────────────────────────────────────┐
│ 请求到达 │
│ │
│ 1. DispatcherServlet.checkMultipart() │
│ │
│ 2. StandardServletMultipartResolver │
│ .resolveMultipart(request) │
│ └─ new StandardMultipartHttpServletRequest(request, true)
│ └─ 构造函数中不解析 │ ← 跳过解析
│ │
│ 3. 控制器执行 │
│ public String upload( │
│ @RequestParam MultipartFile f) { │
│ // 首次访问 file │
│ // → getFile("f") │
│ // → 触发懒加载 │
│ // → parseRequest() │ ← 实际使用时才解析
│ // → request.getParts() │
│ } │
└─────────────────────────────────────────┘8.2 懒加载的适用场景
java
// 场景 1: 可选文件上传
// 控制器中可能不读取文件,仅检查文件是否存在
@PostMapping("/upload-optional")
public String uploadOptional(
@RequestParam("file") MultipartFile file) {
if (file == null || file.isEmpty()) {
// 没有上传文件 → 不需要解析
return "no file";
}
// 如果 resolveLazily=false,即使上面就返回了
// request.getParts() 已经调用过,文件已经解析了
// 浪费了解析开销
// 如果 resolveLazily=true
// 只有到这里才实际解析文件
return "OK";
}
// 场景 2: 条件性处理
// 根据请求中的其他参数决定是否处理文件
@PostMapping("/conditional-upload")
public String conditionalUpload(
@RequestParam("file") MultipartFile file,
@RequestParam("action") String action) {
if ("skip".equals(action)) {
// 不需要处理文件 → 完全避免解析开销
return "skipped";
}
// 只有 action == "process" 时才解析
return "processed";
}8.3 懒加载的注意事项
java
// 注意 1: 懒加载下,在控制器执行之前不会验证文件大小
// 如果文件超过 maxFileSize → 会在 getFile() 时抛出异常
// 可能不在预期的 @ExceptionHandler 的捕获范围内
// 注意 2: 懒加载下,getParameter() 也会触发解析
// 因为 multipart/form-data 请求中的表单字段也通过 Part 传输
// 注意 3: 参数绑定的影响
// resolveLazily=false: 方法参数 MultipartFile 立即可用
// resolveLazily=true: MultipartFile 代理对象,访问具体方法时才解析
// 建议:
// - 简单场景(文件上传肯定要处理)→ resolveLazily=false(默认)
// - 复杂场景(根据条件决定是否处理文件)→ resolveLazily=true总结
| # | 细节点 | 核心要点 |
|---|---|---|
| ① | @ConditionalOnMissingBean | 检查 MultipartConfigElement / StandardServletMultipartResolver / CommonsMultipartResolver 任意一个自定义,自动配置跳过 |
| ② | 5 个核心属性 | maxFileSize(1MB)、maxRequestSize(10MB)、fileSizeThreshold(0)、location("")、resolveLazily(false) |
| ③ | fileSizeThreshold 字节单位 | DataSize.parse("256KB") → 262144 bytes,超过则写入磁盘 |
| ④ | MultipartConfigElement 创建 | 四个参数传入 @MultipartConfig → Tomcat Wrapper.setMultipartConfigElement() |
| ⑤ | parseRequest() 时机 | request.getParts() 首次调用时触发,由 Servlet 容器解析 multipart 请求体 |
| ⑥ | 懒加载机制 | resolveLazily=false 构造函数解析,true 首次 getFile()/getFiles()/getParameter() 时解析 |
| ⑦ | 4 种类型解析 | MultipartFile(单个)、Part(原生)、MultipartFile[](多文件)、Part[](原生集合) |
| ⑧ | resolveLazily=true | 延迟解析,适用于条件性文件处理,避免不必要的 multipart 解析开销 |