Spring MVC文件请求处理详解:MultipartResolver
创始人
2024-03-05 21:36:01
0

org.springframework.web.multipart.MultipartResolver是Spring-Web针对RFC1867实现的多文件上传解决策略。

1 使用场景#

前端上传文件时,无论是使用比较传统的表单,还是使用FormData对象,其本质都是发送一个multipart/form-data请求。
例如,前端模拟上传代码如下:

var formdata = new FormData();
formdata.append("key1", "value1");
formdata.append("key2", "value2");
formdata.append("file1", fileInput.files[0], "/d:/Downloads/rfc1867.pdf");
formdata.append("file2", fileInput.files[0], "/d:/Downloads/rfc1314.pdf");var requestOptions = {method: 'POST',body: formdata,redirect: 'follow'
};fetch("http://localhost:10001/file/upload", requestOptions).then(response => response.text()).then(result => console.log(result)).catch(error => console.log('error', error));

实际会发送如下HTTP请求:

POST /file/upload HTTP/1.1
Host: localhost:10001
Content-Length: 536
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key1"value1
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key2"value2
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file1"; filename="/d:/Downloads/rfc1867.pdf"
Content-Type: application/pdf(data)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file2"; filename="/d:/Downloads/rfc1314.pdf"
Content-Type: application/pdf(data)
----WebKitFormBoundary7MA4YWxkTrZu0gW

在后端可以通过MultipartHttpServletRequest接收文件:

@RestController  
@RequestMapping("file")  
public class FileUploadController {   @RequestMapping("/upload")  public String upload(MultipartHttpServletRequest request) {  // 获取非文件参数  String value1 = request.getParameter("key1");  System.out.println(value1); // value1  String value2 = request.getParameter("key2");  System.out.println(value2); // value2  // 获取文件  MultipartFile file1 = request.getFile("file1");  System.out.println(file1 != null ? file1.getOriginalFilename() : "null"); // rfc1867.pdf  MultipartFile file2 = request.getFile("file2");  System.out.println(file2 != null ? file2.getOriginalFilename() : "null"); // rfc1314.pdf  return "Hello MultipartResolver!";  }  
}

2 MultipartResolver接口#

2.1 MultipartResolver的功能#

org.springframework.web.multipart.MultipartResolver是Spring-Web根据RFC1867规范实现的多文件上传的策略接口。
同时,MultipartResolver是Spring对文件上传处理流程在接口层次的抽象。
也就是说,当涉及到文件上传时,Spring都会使用MultipartResolver接口进行处理,而不涉及具体实现类。
MultipartResolver接口源码如下:

public interface MultipartResolver {  /*** 判断当前HttpServletRequest请求是否是文件请求*/boolean isMultipart(HttpServletRequest request);  /***  将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象*/MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;  /***  清除文件上传产生的临时资源(如服务器本地临时文件)*/void cleanupMultipart(MultipartHttpServletRequest request);  
}

2.2 在DispatcherServlet中的使用#

DispatcherServlet中持有MultipartResolver成员变量:

public class DispatcherServlet extends FrameworkServlet {  /** Well-known name for the MultipartResolver object in the bean factory for this namespace. */  public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";/** MultipartResolver used by this servlet. */  @Nullable  private MultipartResolver multipartResolver;
}

DispatcherServlet在初始化时,会从Spring容器中获取名为multipartResolver的对象(该对象是MultipartResolver实现类),作为文件上传解析器:

/**  * Initialize the MultipartResolver used by this class. * 

If no bean is defined with the given name in the BeanFactory for this namespace, * no multipart handling is provided. */ private void initMultipartResolver(ApplicationContext context) { try { this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); if (logger.isTraceEnabled()) { logger.trace("Detected " + this.multipartResolver); } else if (logger.isDebugEnabled()) { logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName()); } } catch (NoSuchBeanDefinitionException ex) { // Default is no multipart resolver. this.multipartResolver = null; if (logger.isTraceEnabled()) { logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared"); } } }

需要注意的是,如果Spring容器中不存在名为multipartResolver的对象,DispatcherServlet并不会额外指定默认的文件解析器。此时,DispatcherServlet不会对文件上传请求进行处理。也就是说,尽管当前请求是文件请求,也不会被处理成MultipartHttpServletRequest,如果我们在控制层进行强制类型转换,会抛异常。

DispatcherServlet在处理业务时,会按照顺序分别调用这些方法进行文件上传处理,相关核心源码如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {  HttpServletRequest processedRequest = request;boolean multipartRequestParsed = false;try {// 判断&封装文件请求processedRequest = checkMultipart(request);  multipartRequestParsed = (processedRequest != request); // 请求处理……}  finally {   // 清除文件上传产生的临时资源if (multipartRequestParsed) {  cleanupMultipart(processedRequest);  }  }  
}

checkMultipart()方法中,会进行判断、封装文件请求:

/**  * Convert the request into a multipart request, and make multipart resolver available. * 

If no multipart resolver is set, simply use the existing request. * @param request current HTTP request * @return the processed request (multipart wrapper if necessary) * @see MultipartResolver#resolveMultipart */protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) { if (DispatcherType.REQUEST.equals(request.getDispatcherType())) { 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请求是否是文件请求
    1. 是:将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
    2. 不是:不处理
  2. DispatcherServlet对原始HttpServletRequestMultipartHttpServletRequest对象进行业务处理
  3. 业务处理完成,清除文件上传产生的临时资源

2.3 MultipartResolver实现类&配置方式#

Spring提供了两个MultipartResolver实现类:

  • org.springframework.web.multipart.support.StandardServletMultipartResolver:根据Servlet 3.0+ Part Api实现
  • org.springframework.web.multipart.commons.CommonsMultipartResolver:根据Apache Commons FileUpload实现

在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器:

@AutoConfiguration  
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })  
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)  
@ConditionalOnWebApplication(type = Type.SERVLET)  
@EnableConfigurationProperties(MultipartProperties.class)  
public class MultipartAutoConfiguration {  private final MultipartProperties multipartProperties;  public MultipartAutoConfiguration(MultipartProperties multipartProperties) {  this.multipartProperties = multipartProperties;  }  @Bean  @ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })  public MultipartConfigElement multipartConfigElement() {  return this.multipartProperties.createMultipartConfig();  }  @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)  @ConditionalOnMissingBean(MultipartResolver.class)  public StandardServletMultipartResolver multipartResolver() {  StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();  multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());  return multipartResolver;  }  
}

当需要指定其他文件解析器时,只需要引入相关依赖,然后配置一个名为multipartResolverbean对象:

@Bean  
public MultipartResolver multipartResolver() {  MultipartResolver multipartResolver = ...;  return multipartResolver;  
}

接下来,我们分别详细介绍两种实现类的使用和原理。

3 StandardServletMultipartResolver解析器#

3.1 StandardServletMultipartResolver#isMultipart#

StandardServletMultipartResolver解析器的通过判断请求的Content-Type来判断是否是文件请求:

public boolean isMultipart(HttpServletRequest request) {  return StringUtils.startsWithIgnoreCase(request.getContentType(),  (this.strictServletCompliance ? "multipart/form-data" : "multipart/"));  
}

其中,strictServletComplianceStandardServletMultipartResolver的成员变量,默认false,表示是否严格遵守Servlet 3.0规范。简单来说就是对Content-Type校验的严格程度。如果strictServletCompliancefalse,请求头以multipart/开头就满足文件请求条件;如果strictServletCompliancetrue,则需要请求头以multipart/form-data开头。

3.2 StandardServletMultipartResolver#resolveMultipart#

StandardServletMultipartResolver在解析文件请求时,会将原始请求封装成StandardMultipartHttpServletRequest对象:

public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {  return new StandardMultipartHttpServletRequest(request, this.resolveLazily);  
}

需要注意的是,这里传入this.resolveLazily成员变量,表示是否延迟解析。我们可以来看对应构造函数源码:

public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)  throws MultipartException {  super(request);  if (!lazyParsing) {  parseRequest(request);  }  
}

如果需要修改resolveLazily成员变量的值,需要在初始化StandardServletMultipartResolver时指定值。
在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器,此时会从MultipartProperties中读取resolveLazily值。因此,如果是使用Spring Boot 2.0+默认配置的文件解析器,可以在properties.yml文件中指定resolveLazily值:

spring.servlet.multipart.resolve-lazily=true

如果是使用自定义配置的方式配置StandardServletMultipartResolver,则可以在初始化的手动赋值:

@Bean  
public MultipartResolver multipartResolver() {  StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();  multipartResolver.setResolveLazily(true);  return multipartResolver;  
}

3.3 StandardMultipartHttpServletRequest#parseRequest#

resolveLazilytrue时,会马上调用parseRequest()方法会对请求进行实际解析,该方法会完成两件事情:

  1. 使用Servlet 3.0的Part API,获取Part集合
  2. 解析Part对象,封装表单参数和表单文件
private void parseRequest(HttpServletRequest request) {  try {  Collection parts = request.getParts();  this.multipartParameterNames = new LinkedHashSet<>(parts.size());  MultiValueMap files = new LinkedMultiValueMap<>(parts.size());  for (Part part : parts) {  String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);  ContentDisposition disposition = ContentDisposition.parse(headerValue);  String filename = disposition.getFilename();  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);  }  
}

经过parseRequest()方法处理,我们在业务处理时,直接调用StandardMultipartHttpServletRequest接口的getXxx()方法就可以获取表单参数或表单文件信息。

resolveLazilyfalse时,在MultipartResolver#resolveMultipart()阶段并不会进行文件请求解析。也就是说,此时StandardMultipartHttpServletRequest对象的成员变量都是空值。那么,resolveLazilyfalse时文件请求解析是在什么时候完成的呢?
实际上,在调用StandardMultipartHttpServletRequest接口的getXxx()方法时,内部会判断是否已经完成文件请求解析。如果未解析,就会调用partRequest()方法进行解析,例如:

@Override  
public Enumeration getParameterNames() {  if (this.multipartParameterNames == null) {  initializeMultipart();  // parseRequest(getRequest());}  // 业务处理……
}

3.4 HttpServletRequest#getParts#

根据StandardMultipartHttpServletRequest#parseRequest源码可以发现,StandardServletMultipartResolver解析文件请求依靠的是HttpServletRequest#getParts方法。
这是StandardServletMultipartResolver是根据标准Servlet 3.0实现的核心体现。
在Servlet 3.0中定义了javax.servlet.http.Part,用来表示multipart/form-data请求体中的表单数据或文件:

public interface Part {  public InputStream getInputStream() throws IOException;  public String getContentType();  public String getName();  public String getSubmittedFileName();  public long getSize();  public void write(String fileName) throws IOException;  public void delete() throws IOException;  public String getHeader(String name);  public Collection getHeaders(String name);  public Collection getHeaderNames();  
}

javax.servlet.http.HttpServletRequest,提供了获取multipart/form-data请求体各个part的方法:

public interface HttpServletRequest extends ServletRequest {    /**  * Return a collection of all uploaded Parts.     *     * @return A collection of all uploaded Parts.    * @throws IOException  *             if an I/O error occurs  * @throws IllegalStateException  *             if size limits are exceeded or no multipart configuration is  *             provided     * @throws ServletException  *             if the request is not multipart/form-data  * @since Servlet 3.0     */   public Collection getParts() throws IOException, ServletException;  /**  * Gets the named Part or null if the Part does not exist. Triggers upload     * of all Parts.    *     * @param name The name of the Part to obtain  *     * @return The named Part or null if the Part does not exist    * @throws IOException  *             if an I/O error occurs  * @throws IllegalStateException  *             if size limits are exceeded  * @throws ServletException  *             if the request is not multipart/form-data  * @since Servlet 3.0     */    public Part getPart(String name) throws IOException, ServletException;  
}

所有实现标准Servlet 3.0规范的Web服务器,都必须实现getPart()/getParts()方法。也就是说,这些Web服务器在解析请求时,会将multipart/form-data请求体中的表单数据或文件解析成Part对象集合。通过HttpServletRequestgetPart()/getParts()方法,可以获取这些Part对象,进而获取multipart/form-data请求体中的表单数据或文件。
每个Web服务器对Servlet 3.0规范都有自己的实现方式。对于Spring Boot来说,通常使用的是Tomcat/Undertow/Jetty内嵌Web服务器。通常只需要了解这三种服务器的实现方式即可。

3.4.1 Tomcat实现#

Tomcat是Spring Boot默认使用的内嵌Web服务器,只需要引入如下依赖:

  org.springframework.boot  spring-boot-starter-web  

会默认引入Tomcat依赖:

  org.springframework.boot  spring-boot-starter-tomcat  

Tomcat解析文件请求的核心在于org.apache.catalina.connector.Request#parseParts方法,核心代码如下:

// 1、创建ServletFileUpload文件上传对象
DiskFileItemFactory factory = new DiskFileItemFactory();  
try {  factory.setRepository(location.getCanonicalFile());  
} catch (IOException ioe) {  parameters.setParseFailedReason(FailReason.IO_ERROR);  partsParseException = ioe;  return;  
}  
factory.setSizeThreshold(mce.getFileSizeThreshold());  ServletFileUpload upload = new ServletFileUpload();  
upload.setFileItemFactory(factory);  
upload.setFileSizeMax(mce.getMaxFileSize());  
upload.setSizeMax(mce.getMaxRequestSize());
this.parts = new ArrayList<>();  
try {  // 2、解析文件请求List items =  upload.parseRequest(new ServletRequestContext(this));// 3、封装Part对象for (FileItem item : items) {  ApplicationPart part = new ApplicationPart(item, location);  this.parts.add(part);  }  }  success = true;  
}

核心步骤如下:

  1. 创建ServletFileUpload文件上传对象
  2. 解析文件请求
  3. 封装Part对象

org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest会进行实际解析文件请求:

public List parseRequest(final RequestContext ctx) throws FileUploadException {  final List items = new ArrayList<>();  boolean successful = false;  try {  final FileItemIterator iter = getItemIterator(ctx);  final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),  "No FileItemFactory has been set.");  final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];  while (iter.hasNext()) {  final FileItemStream item = iter.next();  // Don't use getName() here to prevent an InvalidFileNameException.  final String fileName = item.getName();  final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),  item.isFormField(), fileName);  items.add(fileItem);  try {  Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);  } catch (final FileUploadIOException e) {  throw (FileUploadException) e.getCause();  } catch (final IOException e) {  throw new IOFileUploadException(String.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;  }
}

简单来说,Tomcat会使用java.io.InputStreamjava.io.OutputStream(传统IO流)将multipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回。
也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

3.4.2 Undertow实现#

为了使用Undertow服务器,需要引入如下依赖:

  org.springframework.boot  spring-boot-starter-web      org.springframework.boot  spring-boot-starter-tomcat      
  
  org.springframework.boot  spring-boot-starter-undertow  

Undertow解析文件请求的核心在于io.undertow.servlet.spec.HttpServletRequestImpl#loadParts方法,核心代码如下

final List parts = new ArrayList<>();  
String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE);  
if (mimeType != null && mimeType.startsWith(MultiPartParserDefinition.MULTIPART_FORM_DATA)) {  // 1、解析文件请求,封装FormData对象FormData formData = parseFormData();  // 2、封装Part对象if(formData != null) {  for (final String namedPart : formData) {  for (FormData.FormValue part : formData.get(namedPart)) {  parts.add(new PartImpl(namedPart,  part,  requestContext.getOriginalServletPathMatch().getServletChain().getManagedServlet().getMultipartConfig(),  servletContext, this));  }  }  }  
} else {  throw UndertowServletMessages.MESSAGES.notAMultiPartRequest();  
}  
this.parts = parts;

核心步骤如下:

  1. 解析文件请求,封装FormData对象
  2. 封装Part对象

io.undertow.servlet.spec.HttpServletRequestImpl#parseFormData方法会进行实际解析文件请求,核心代码如下:

final FormDataParser parser = originalServlet.getFormParserFactory().createParser(exchange) 
try {  return parsedFormData = parser.parseBlocking();
}

io.undertow.server.handlers.form.MultiPartParserDefinition.MultiPartUploadHandler#parseBlocking核心代码如下:

InputStream inputStream = exchange.getInputStream();
try (PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().getArrayBackedPool().allocate()){  ByteBuffer buf = pooled.getBuffer();  while (true) {  buf.clear();  int c = inputStream.read(buf.array(), buf.arrayOffset(), buf.remaining());  if (c == -1) {  if (parser.isComplete()) {  break;  } else {  throw UndertowMessages.MESSAGES.connectionTerminatedReadingMultiPartData();  }  } else if (c != 0) {  buf.limit(c);  parser.parse(buf);  }  }  exchange.putAttachment(FORM_DATA, data);  
} 
return exchange.getAttachment(FORM_DATA);

在这个过程中,Undertow会使用java.io.InputStreamjava.io.OutputStream(传统IO流),结合java.nio.ByteBuffermultipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回(具体细节可以继续深入阅读相关源码)。
也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

3.4.2 Jetty实现#

为了使用Jetty服务器,需要引入如下依赖:

  org.springframework.boot  spring-boot-starter-web      org.springframework.boot  spring-boot-starter-tomcat      
  
  org.springframework.boot  spring-boot-starter-jetty  

Jetty解析文件请求的核心在于org.eclipse.jetty.server.Request#getParts方法,核心代码如下

MultipartConfigElement config = (MultipartConfigElement)this.getAttribute("org.eclipse.jetty.multipartConfig");  
this._multiParts = this.newMultiParts(config);
// 省略……
return this._multiParts.getParts();

org.eclipse.jetty.server.Request#newMultiParts会创建文件解析器:

private MultiParts newMultiParts(MultipartConfigElement config) throws IOException {  MultiPartFormDataCompliance compliance = this.getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance(); switch(compliance) {  case RFC7578:  return new MultiPartsHttpParser(this.getInputStream(), this.getContentType(), config, this._context != null ? (File)this._context.getAttribute("javax.servlet.context.tempdir") : null, this);  case LEGACY:  default:  return new MultiPartsUtilParser(this.getInputStream(), this.getContentType(), config, this._context != null ? (File)this._context.getAttribute("javax.servlet.context.tempdir") : null, this);  }  
}

org.eclipse.jetty.server.MultiParts.MultiPartsHttpParser#getPartsorg.eclipse.jetty.server.MultiParts.MultiPartsUtilParser#getParts则会进行文件请求解析:

public Collection getParts() throws IOException {  Collection parts = this._httpParser.getParts();  this.setNonComplianceViolationsOnRequest();  return parts;  
}public Collection getParts() throws IOException {  Collection parts = this._utilParser.getParts();  this.setNonComplianceViolationsOnRequest();  return parts;  
}

在这个过程中,Jetty会使用java.io.InputStreamjava.io.OutputStream(传统IO流),结合java.nio.ByteBuffermultipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回。
也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

3.5 StandardServletMultipartResolver#cleanupMultipart#

StandardServletMultipartResolver#cleanupMultipart方法会将临时文件删除:

public 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);  }  }  
}

4 CommonsMultipartResolver解析器#

为了使用CommonsMultipartResolver解析器,除了基础的spring-boot-starter-web,还需要额外引入如下依赖:

  commons-fileupload  commons-fileupload  1.4  

然后,配置名为multipartResolver的bean(此时Spring Boot不会添加默认文件解析器):

@Bean  
public MultipartResolver multipartResolver() {  CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();  // 文件请求解析配置:multipartResolver.setXxx()  multipartResolver.setResolveLazily(true);  return multipartResolver;  
}

4.1 CommonsMultipartResolver#isMultipart#

CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求,源码如下:

public boolean isMultipart(HttpServletRequest request) {  return (this.supportedMethods != null ?  this.supportedMethods.contains(request.getMethod()) &&  FileUploadBase.isMultipartContent(new ServletRequestContext(request)) :  ServletFileUpload.isMultipartContent(request));  
}

supportedMethods成员变量表示支持的请求方法,默认为null,可以在初始化时指定。
supportedMethodsnull时,即在默认情况下,会调用ServletFileUpload.isMultipartContent()方法进行判断。此时文件请求的满足条件为:

  1. 请求方法为POST
  2. 请求头Content-Type为以multipart/开头

supportedMethods不为null时,文件请求满足条件为:

  1. 请求方法在supportedMethods列表中
  2. 请求头Content-Type为以multipart/开头

4.2 CommonsMultipartResolver#resolveMultipart#

CommonsMultipartResolver在解析文件请求时,会将原始请求封装成DefaultMultipartHttpServletRequest对象:

public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {  Assert.notNull(request, "Request must not be null");  if (this.resolveLazily) {  return new DefaultMultipartHttpServletRequest(request) {  @Override  protected 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());  }  
}

StandardServletMultipartResolver相同,CommonsMultipartResolverresolveLazily成员变量也表示是否会马上解析文件。
resolveLazilyfalse时,即默认情况下,不会立即解析文件,只是会将原始请求进行简单封装。只有在调用DefaultMultipartHttpServletRequest#getXxx方法时,会判断文件是否已经解析。如果没有解析,会调用DefaultMultipartHttpServletRequest#initializeMultipart进行解析。
resolveLazilytrue时,会立即调用CommonsMultipartResolver#parseRequest方法进行文件解析。

4.3 CommonsMultipartResolver#parseRequest#

CommonsMultipartResolver#parseRequest方法会进行文件请求解析,总的来说包括两个步骤:

  1. 解析文件请求
  2. 封装响应
List fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);  
return parseFileItems(fileItems, encoding);

深入阅读源码可以发现,在解析文件请求时,会采用与StandardServletMultipartResolver+Tomcat相同的方式保存临时文件:

public List parseRequest(RequestContext ctx)  throws FileUploadException {  List items = new ArrayList();  boolean successful = false;  try {  FileItemIterator iter = getItemIterator(ctx);  FileItemFactory fac = getFileItemFactory();  if (fac == null) {  throw new NullPointerException("No FileItemFactory has been set.");  }  while (iter.hasNext()) {  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 {  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?  }  }  }  }  
}

4.4 CommonsMultipartResolver#cleanupMultipart#

CommonsMultipartResolver#cleanupMultipart方法会将临时文件删除:

public void cleanupMultipart(MultipartHttpServletRequest request) {  if (!(request instanceof AbstractMultipartHttpServletRequest) ||  ((AbstractMultipartHttpServletRequest) request).isResolved()) {  try {  cleanupFileItems(request.getMultiFileMap());  }  catch (Throwable ex) {  logger.warn("Failed to perform multipart cleanup for servlet request", ex);  }  }  
}

相关内容

热门资讯

海南自贸区涨停潮背后:政策红利... 12月22日上午,A股市场迎来久违的“多点开花”——三大指数齐涨,而最引人注目的,莫过于海南自贸区板...
深圳:深入实施跨境贸易投资高水... 人民财讯12月23日电,深圳市人民政府印发《深圳市进一步加大吸引和利用外资实施办法》,其中提出,进一...
国家发展改革委等三部门完善幼儿... 中新网12月23日电 据国家发展改革委网站消息,近日,为深入贯彻党的二十大和二十届历次全会精神,落实...
建工修复(300958)披露累... 截至2025年12月23日收盘,建工修复(300958)报收于12.39元,较前一交易日下跌0.88...
红豆股份(600400)披露拟... 截至2025年12月23日收盘,红豆股份(600400)报收于2.4元,较前一交易日下跌2.83%,...
凌钢股份与江苏银行深圳分行纠纷... 12月23日,凌钢股份(600231)发布公告,近期公司收到辽宁省朝阳市中级人民法院的民事裁定书,案...
科蓝软件:科蓝盛合陷合同纠纷 ... 12月23日,科蓝软件(300663)发布公告,控股股东一致行动人宁波科蓝盛合投资管理合伙企业(有限...
广东潮州通报“因购物纠纷引发的... 本文转自【看潮州客户端】; 情况简讯 记者获悉,12月18日,在市区枫春路发生一起因购物纠纷引发的殴...