Spring MVC文件请求处理详解:MultipartResolver

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. * <p>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. * <p>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<Part> parts = request.getParts();         this.multipartParameterNames = new LinkedHashSet<>(parts.size());         MultiValueMap<String, MultipartFile> 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<String> 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<String> getHeaders(String name);   	public Collection<String> 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<Part> 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服务器,只需要引入如下依赖:

<dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-web</artifactId>   </dependency> 

会默认引入Tomcat依赖:

<dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-tomcat</artifactId>   </dependency> 

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<FileItem> 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<FileItem> parseRequest(final RequestContext ctx) throws FileUploadException {       final List<FileItem> 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服务器,需要引入如下依赖:

<dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-web</artifactId>      <exclusions>         <exclusion>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-tomcat</artifactId>         </exclusion>      </exclusions>   </dependency>   <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-undertow</artifactId>   </dependency> 

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

final List<Part> 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服务器,需要引入如下依赖:

<dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-web</artifactId>      <exclusions>         <exclusion>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-tomcat</artifactId>         </exclusion>      </exclusions>   </dependency>   <dependency>      <groupId>org.springframework.boot</groupId>      <artifactId>spring-boot-starter-jetty</artifactId>   </dependency> 

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<Part> getParts() throws IOException {       Collection<Part> parts = this._httpParser.getParts();       this.setNonComplianceViolationsOnRequest();       return parts;   }  public Collection<Part> getParts() throws IOException {       Collection<Part> 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,还需要额外引入如下依赖:

<dependency>      <groupId>commons-fileupload</groupId>      <artifactId>commons-fileupload</artifactId>      <version>1.4</version>   </dependency> 

然后,配置名为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时,文件请求满足条件为:
  3. 请求方法在supportedMethods列表中
  4. 请求头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<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);   return parseFileItems(fileItems, encoding); 

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

public List<FileItem> parseRequest(RequestContext ctx)           throws FileUploadException {       List<FileItem> items = new ArrayList<FileItem>();       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);         }      }   } 

发表评论

评论已关闭。

相关文章