Spring Boot中的MultipartResolver是一个用于解析multipart/form-data类型请求的策略接口,通常用于文件上传。
对应后端使用MultipartFile对象接收。
@RequestMapping("/upload")public String uploadFile(MultipartFile file) throws IOException {String fileName = file.getOriginalFilename();String filePath = "D:\\";File file1 = new File(filePath+"\\"+fileName);file.transferTo(file1);return "ok";}
一、MultipartResolver接口
MultipartResolver是个接口,不做任何实现。
public interface MultipartResolver { /*** 判断当前HttpServletRequest请求是否是文件请求*/boolean isMultipart(HttpServletRequest request); /*** 将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象*/MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException; /*** 清除文件上传产生的临时资源(如服务器本地临时文件)*/void cleanupMultipart(MultipartHttpServletRequest request);
}
MultipartResolver在DispatcherServlet使用
我们都知道请求首先到达DispatcherServlet,它负责协调和组织不同组件完成请求处理并返回响应工作,所以DispatcherServlet中持有MultipartResolver成员变量,
在onRefresh中完成包括MultipartResolver在内的9个组件。
在doDispatch中调用checkMultipart(request); 方法将请求转换为 multipart 请求,如该请求不是multipart 请求则返回原request
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");}}else if (hasMultipartException(request)) {logger.debug("Multipart resolution previously failed for current request - " +"skipping re-resolution for undisturbed error rendering");}else {try {return this.multipartResolver.resolveMultipart(request);}catch (MultipartException ex) {if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {logger.debug("Multipart resolution failed for error dispatch", ex);// Keep processing error dispatch with regular request handle below}else {throw ex;}}}}// If not returned before: return original request.return request;}
DispatcherServlet处理文件请求会经过以下步骤:
1、判断当前HttpServletRequest请求是否是文件请求
- 是:将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
- 不是:不处理
2、DispatcherServlet对原始HttpServletRequest或MultipartHttpServletRequest对象进行业务处理
3、业务处理完成,清除文件上传产生的临时资源
二、MultipartResolver的实现类
Spring提供了两个MultipartResolver实现类:
- StandardServletMultipartResolver:根据Servlet 3.0+ Part Api实现(默认使用)
- CommonsMultipartResolver:根据Apache Commons FileUpload实现,需要引入相关的依赖
三、 StandardServletMultipartResolver
StandardServletMultipartResolver是根据Servlet 3.0+ Part Api实现,也是我们默认使用文件解析器。
public class StandardServletMultipartResolver implements MultipartResolver {private boolean resolveLazily = false;/*** 设置是否延迟解析* 可通过配置文件进行设置spring.servlet.multipart.resolve-lazily=true* @since 3.2.9*/public void setResolveLazily(boolean resolveLazily) {this.resolveLazily = resolveLazily;}/*** 判断是否是multipart请求*/@Overridepublic boolean isMultipart(HttpServletRequest request) {return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");}/*** 返回StandardMultipartHttpServletRequest请求(主要部分)*/@Overridepublic MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {return new StandardMultipartHttpServletRequest(request, this.resolveLazily);}/*** 清理临时文件*/@Overridepublic void cleanupMultipart(MultipartHttpServletRequest request) {if (!(request instanceof AbstractMultipartHttpServletRequest) ||((AbstractMultipartHttpServletRequest) request).isResolved()) {// To be on the safe side: explicitly delete the parts,// but only actual file parts (for Resin compatibility)try {for (Part part : request.getParts()) {if (request.getFile(part.getName()) != null) {part.delete();}}}catch (Throwable ex) {LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);}}}}
3.1 StandardMultipartHttpServletRequest
构造方法
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)throws MultipartException {super(request);if (!lazyParsing) {parseRequest(request);}}
当resolveLazily为false时,在MultipartResolver#resolveMultipart()阶段并不会进行文件请求解析。也就是说,此时StandardMultipartHttpServletRequest对象的成员变量都是空值。那么,resolveLazily为false时文件请求解析是在什么时候完成的呢?
实际上,在调用StandardMultipartHttpServletRequest接口的getXxx()方法时,内部会判断是否已经完成文件请求解析。如果未解析,就会调用partRequest()方法进行解析,例如:
StandardMultipartHttpServletRequest#parseRequest
private void parseRequest(HttpServletRequest request) {try {//调用getParts()方法从请求中获取所有的部分(parts),这些部分可能包括文件和表单数据Collection<Part> parts = request.getParts();//初始化集合存储所有非文件的参数名this.multipartParameterNames = new LinkedHashSet<>(parts.size());//存储每个文件的名称及其对应的MultipartFile对象。MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());//对于每个部分,获取其Content-Disposition头,并解析出文件名。for (Part part : parts) {String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);ContentDisposition disposition = ContentDisposition.parse(headerValue);String filename = disposition.getFilename();//如果文件名不为null,检测是否需要解码,添加到files中if (filename != null) {if (filename.startsWith("=?") && filename.endsWith("?=")) {filename = MimeDelegate.decode(filename);}files.add(part.getName(), new StandardMultipartFile(part, filename));}//处理非文件参数else {this.multipartParameterNames.add(part.getName());}}setMultipartFiles(files);}catch (Throwable ex) {handleParseFailure(ex);}}
3.2 CommonsMultipartResolver
CommonsMultipartResolver根据Apache Commons FileUpload实现,可以处理大文件、文件流等。功能比较强大
首先需要引入了commons-fileupload 依赖:
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version>
</dependency>
编写配置类
@Configuration
public class MultipartResolverConfig {@Beanpublic MultipartResolver multipartResolver() {CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();// 文件请求解析配置:multipartResolver.setXxx() multipartResolver.setResolveLazily(true);// 设置编码格式multipartResolver.setDefaultEncoding("UTF-8");// 设置写入内存的最大值(单位Byte),超过此值则写入硬盘临时文件multipartResolver.setMaxInMemorySize(1024 * 1024);// 设置上传文件最大值multipartResolver.setMaxUploadSize(1024 * 1024 * 1024);return multipartResolver;}}
CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求,源码如下:
@Overridepublic boolean isMultipart(HttpServletRequest request) {return ServletFileUpload.isMultipartContent(request);}public static final boolean isMultipartContent(HttpServletRequest request) {return !"POST".equalsIgnoreCase(request.getMethod()) ? false : FileUploadBase.isMultipartContent(new ServletRequestContext(request));}
3.2.1CommonsMultipartResolver#resolveMultipart#
CommonsMultipartResolver在解析文件请求时,会将原始请求封装成DefaultMultipartHttpServletRequest对象:
@Overridepublic MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {Assert.notNull(request, "Request must not be null");// 判断是否是懒加载if (this.resolveLazily) {return new DefaultMultipartHttpServletRequest(request) {@Overrideprotected void initializeMultipart() {MultipartParsingResult parsingResult = parseRequest(request);setMultipartFiles(parsingResult.getMultipartFiles());setMultipartParameters(parsingResult.getMultipartParameters());setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());}};}else {MultipartParsingResult parsingResult = parseRequest(request);return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());}}
3.2.2 CommonsMultipartResolver#parseRequest
CommonsMultipartResolver使用流式处理文件,可以避免内存溢出
问题背景: 文件上传过程中,如果没有采用合适的策略,处理大文件时容易导致内存溢出(OutOfMemoryError)。这通常发生在将整个文件内容加载到内存中再进行处理时。
解决方法: CommonsMultipartResolver 使用了 流式处理,即它不会一次性将整个文件加载到内存中,而是通过流(stream)方式逐步读取文件的内容。这种方式确保了即使上传的是大文件,系统内存也不会被大量占用。这种流式读取方式能有效避免内存溢出问题。
public List<FileItem> parseRequest(RequestContext ctx)throws FileUploadException {List<FileItem> items = new ArrayList<FileItem>();boolean successful = false;try {// 获取文件迭代器,用于遍历 multipart/form-data 请求中的每个部分。每个部分可能是一个文件或者表单字段。FileItemIterator iter = getItemIterator(ctx);FileItemFactory fac = getFileItemFactory();if (fac == null) {throw new NullPointerException("No FileItemFactory has been set.");}while (iter.hasNext()) {/*** 使用 iter.hasNext() 遍历每个文件部分,item 代表当前处理的文件流。* fileName 从当前文件部分中获取文件名(这里没有使用 getName() 是为了避免异常)。* 调用 fac.createItem() 创建一个 FileItem,传入字段名、内容类型、是否是表单字段以及文件名,然后将 fileItem 添加到 items 列表中*/final FileItemStream item = iter.next();// Don't use getName() here to prevent an InvalidFileNameException.final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),item.isFormField(), fileName);items.add(fileItem);try {// 将 item 的输入流(文件数据)通过 Streams.copy 复制到 fileItem 的输出流中,完成文件上传。Streams.copy(item.openStream(), fileItem.getOutputStream(), true);} catch (FileUploadIOException e) {throw (FileUploadException) e.getCause();} catch (IOException e) {throw new IOFileUploadException(format("Processing of %s request failed. %s",MULTIPART_FORM_DATA, e.getMessage()), e);}final FileItemHeaders fih = item.getHeaders();fileItem.setHeaders(fih);}successful = true;return items;} catch (FileUploadIOException e) {throw (FileUploadException) e.getCause();} catch (IOException e) {throw new FileUploadException(e.getMessage(), e);} finally {if (!successful) {for (FileItem fileItem : items) {try {fileItem.delete();} catch (Exception ignored) {// ignored TODO perhaps add to tracker delete failure list somehow?}}}}}