欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Spring?MVC文件請(qǐng)求處理MultipartResolver詳解

 更新時(shí)間:2022年11月29日 08:37:41   作者:Xianhuii  
這篇文章主要介紹了Spring?MVC文件請(qǐng)求處理詳解:MultipartResolver,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下

org.springframework.web.multipart.MultipartResolver是Spring-Web針對(duì)RFC1867實(shí)現(xiàn)的多文件上傳解決策略。

1 使用場(chǎng)景

前端上傳文件時(shí),無(wú)論是使用比較傳統(tǒng)的表單,還是使用FormData對(duì)象,其本質(zhì)都是發(fā)送一個(gè)multipart/form-data請(qǐng)求。
例如,前端模擬上傳代碼如下:

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

實(shí)際會(huì)發(fā)送如下HTTP請(qǐng)求:

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

在后端可以通過(guò)MultipartHttpServletRequest接收文件:

@RestController  
@RequestMapping("file")  
public class FileUploadController {   
    @RequestMapping("/upload")  
    public String upload(MultipartHttpServletRequest request) {  
        // 獲取非文件參數(shù)  
        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根據(jù)RFC1867規(guī)范實(shí)現(xiàn)的多文件上傳的策略接口。
同時(shí),MultipartResolver是Spring對(duì)文件上傳處理流程在接口層次的抽象。
也就是說(shuō),當(dāng)涉及到文件上傳時(shí),Spring都會(huì)使用MultipartResolver接口進(jìn)行處理,而不涉及具體實(shí)現(xiàn)類(lèi)。 MultipartResolver`接口源碼如下:

public interface MultipartResolver {  
	/**
	* 判斷當(dāng)前HttpServletRequest請(qǐng)求是否是文件請(qǐng)求
	*/
    boolean isMultipart(HttpServletRequest request);  
	/**
	*  將當(dāng)前HttpServletRequest請(qǐng)求的數(shù)據(jù)(文件和普通參數(shù))封裝成MultipartHttpServletRequest對(duì)象
	*/
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;  
	/**
	*  清除文件上傳產(chǎn)生的臨時(shí)資源(如服務(wù)器本地臨時(shí)文件)
	*/
    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在初始化時(shí),會(huì)從Spring容器中獲取名為multipartResolver的對(duì)象(該對(duì)象是MultipartResolver實(shí)現(xiàn)類(lèi)),作為文件上傳解析器:

/**  
 * 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的對(duì)象,DispatcherServlet并不會(huì)額外指定默認(rèn)的文件解析器。此時(shí),DispatcherServlet不會(huì)對(duì)文件上傳請(qǐng)求進(jìn)行處理。也就是說(shuō),盡管當(dāng)前請(qǐng)求是文件請(qǐng)求,也不會(huì)被處理成MultipartHttpServletRequest,如果我們?cè)诳刂茖舆M(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換,會(huì)拋異常。

DispatcherServlet在處理業(yè)務(wù)時(shí),會(huì)按照順序分別調(diào)用這些方法進(jìn)行文件上傳處理,相關(guān)核心源碼如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {  
   HttpServletRequest processedRequest = request;
   boolean multipartRequestParsed = false;
   try {
		// 判斷&封裝文件請(qǐng)求
         processedRequest = checkMultipart(request);  
         multipartRequestParsed = (processedRequest != request); 
         // 請(qǐng)求處理……
   }  
   finally {   
         // 清除文件上傳產(chǎn)生的臨時(shí)資源
         if (multipartRequestParsed) {  
            cleanupMultipart(processedRequest);  
         }  
   }  
}

checkMultipart()方法中,會(huì)進(jìn)行判斷、封裝文件請(qǐng)求:

/**  
 * 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;  
}

總的來(lái)說(shuō),DispatcherServlet處理文件請(qǐng)求會(huì)經(jīng)過(guò)以下步驟:

  • 判斷當(dāng)前HttpServletRequest請(qǐng)求是否是文件請(qǐng)求

    是:將當(dāng)前HttpServletRequest請(qǐng)求的數(shù)據(jù)(文件和普通參數(shù))封裝成MultipartHttpServletRequest對(duì)象

    不是:不處理

  • DispatcherServlet對(duì)原始HttpServletRequestMultipartHttpServletRequest對(duì)象進(jìn)行業(yè)務(wù)處理
  • 業(yè)務(wù)處理完成,清除文件上傳產(chǎn)生的臨時(shí)資源

2.3 MultipartResolver實(shí)現(xiàn)類(lèi)&配置方式

Spring提供了兩個(gè)MultipartResolver實(shí)現(xiàn)類(lèi):

  • org.springframework.web.multipart.support.StandardServletMultipartResolver:根據(jù)Servlet 3.0+ Part Api實(shí)現(xiàn)
  • org.springframework.web.multipart.commons.CommonsMultipartResolver:根據(jù)Apache Commons FileUpload實(shí)現(xiàn)

在Spring Boot 2.0+中,默認(rèn)會(huì)在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中創(chuàng)建StandardServletMultipartResolver作為默認(rèn)文件解析器:

@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;  
   }  
}

當(dāng)需要指定其他文件解析器時(shí),只需要引入相關(guān)依賴(lài),然后配置一個(gè)名為multipartResolverbean對(duì)象:

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

接下來(lái),我們分別詳細(xì)介紹兩種實(shí)現(xiàn)類(lèi)的使用和原理。

3 StandardServletMultipartResolver解析器

3.1 StandardServletMultipartResolver#isMultipart#

StandardServletMultipartResolver解析器的通過(guò)判斷請(qǐng)求的Content-Type來(lái)判斷是否是文件請(qǐng)求:

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

其中,strictServletComplianceStandardServletMultipartResolver的成員變量,默認(rèn)false,表示是否嚴(yán)格遵守Servlet 3.0規(guī)范。簡(jiǎn)單來(lái)說(shuō)就是對(duì)Content-Type校驗(yàn)的嚴(yán)格程度。如果strictServletCompliancefalse,請(qǐng)求頭以multipart/開(kāi)頭就滿(mǎn)足文件請(qǐng)求條件;如果strictServletCompliancetrue,則需要請(qǐng)求頭以multipart/form-data開(kāi)頭。

3.2 StandardServletMultipartResolver#resolveMultipart

StandardServletMultipartResolver在解析文件請(qǐng)求時(shí),會(huì)將原始請(qǐng)求封裝成StandardMultipartHttpServletRequest對(duì)象:

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

需要注意的是,這里傳入this.resolveLazily成員變量,表示是否延遲解析。我們可以來(lái)看對(duì)應(yīng)構(gòu)造函數(shù)源碼:

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

如果需要修改resolveLazily成員變量的值,需要在初始化StandardServletMultipartResolver時(shí)指定值。
在Spring Boot 2.0+中,默認(rèn)會(huì)在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中創(chuàng)建StandardServletMultipartResolver作為默認(rèn)文件解析器,此時(shí)會(huì)從MultipartProperties中讀取resolveLazily值。因此,如果是使用Spring Boot 2.0+默認(rèn)配置的文件解析器,可以在properties.yml文件中指定resolveLazily值:

spring.servlet.multipart.resolve-lazily=true

如果是使用自定義配置的方式配置StandardServletMultipartResolver,則可以在初始化的手動(dòng)賦值:

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

3.3 StandardMultipartHttpServletRequest#parseRequest

當(dāng)resolveLazilytrue時(shí),會(huì)馬上調(diào)用parseRequest()方法會(huì)對(duì)請(qǐng)求進(jìn)行實(shí)際解析,該方法會(huì)完成兩件事情:

  • 使用Servlet 3.0的Part API,獲取Part集合
  • 解析Part對(duì)象,封裝表單參數(shù)和表單文件
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);  
   }  
}

經(jīng)過(guò)parseRequest()方法處理,我們?cè)跇I(yè)務(wù)處理時(shí),直接調(diào)用StandardMultipartHttpServletRequest接口的getXxx()方法就可以獲取表單參數(shù)或表單文件信息。

當(dāng)resolveLazilyfalse時(shí),在MultipartResolver#resolveMultipart()階段并不會(huì)進(jìn)行文件請(qǐng)求解析。也就是說(shuō),此時(shí)StandardMultipartHttpServletRequest對(duì)象的成員變量都是空值。那么,resolveLazilyfalse時(shí)文件請(qǐng)求解析是在什么時(shí)候完成的呢?
實(shí)際上,在調(diào)用StandardMultipartHttpServletRequest接口的getXxx()方法時(shí),內(nèi)部會(huì)判斷是否已經(jīng)完成文件請(qǐng)求解析。如果未解析,就會(huì)調(diào)用partRequest()方法進(jìn)行解析,例如:

@Override  
public Enumeration<String> getParameterNames() {  
   if (this.multipartParameterNames == null) {  
      initializeMultipart();  // parseRequest(getRequest());
   }  
   // 業(yè)務(wù)處理……
}

3.4 HttpServletRequest#getParts

根據(jù)StandardMultipartHttpServletRequest#parseRequest源碼可以發(fā)現(xiàn),StandardServletMultipartResolver解析文件請(qǐng)求依靠的是HttpServletRequest#getParts方法。
這是StandardServletMultipartResolver是根據(jù)標(biāo)準(zhǔn)Servlet 3.0實(shí)現(xiàn)的核心體現(xiàn)。
在Servlet 3.0中定義了javax.servlet.http.Part,用來(lái)表示multipart/form-data請(qǐng)求體中的表單數(shù)據(jù)或文件:

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請(qǐng)求體各個(gè)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;  
}

所有實(shí)現(xiàn)標(biāo)準(zhǔn)Servlet 3.0規(guī)范的Web服務(wù)器,都必須實(shí)現(xiàn)getPart()/getParts()方法。也就是說(shuō),這些Web服務(wù)器在解析請(qǐng)求時(shí),會(huì)將multipart/form-data請(qǐng)求體中的表單數(shù)據(jù)或文件解析成Part對(duì)象集合。通過(guò)HttpServletRequestgetPart()/getParts()方法,可以獲取這些Part對(duì)象,進(jìn)而獲取multipart/form-data請(qǐng)求體中的表單數(shù)據(jù)或文件。
每個(gè)Web服務(wù)器對(duì)Servlet 3.0規(guī)范都有自己的實(shí)現(xiàn)方式。對(duì)于Spring Boot來(lái)說(shuō),通常使用的是Tomcat/Undertow/Jetty內(nèi)嵌Web服務(wù)器。通常只需要了解這三種服務(wù)器的實(shí)現(xiàn)方式即可。

3.4.1 Tomcat實(shí)現(xiàn)

Tomcat是Spring Boot默認(rèn)使用的內(nèi)嵌Web服務(wù)器,只需要引入如下依賴(lài):

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

會(huì)默認(rèn)引入Tomcat依賴(lài):

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

Tomcat解析文件請(qǐng)求的核心在于org.apache.catalina.connector.Request#parseParts方法,核心代碼如下:

// 1、創(chuàng)建ServletFileUpload文件上傳對(duì)象
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、解析文件請(qǐng)求
    List<FileItem> items =  
            upload.parseRequest(new ServletRequestContext(this));
    // 3、封裝Part對(duì)象
    for (FileItem item : items) {  
        ApplicationPart part = new ApplicationPart(item, location);  
        this.parts.add(part);  
        }  
    }  
    success = true;  
}

核心步驟如下:

  • 創(chuàng)建ServletFileUpload文件上傳對(duì)象
  • 解析文件請(qǐng)求
  • 封裝Part對(duì)象
    org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest會(huì)進(jìn)行實(shí)際解析文件請(qǐng)求:
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;  
    }
}

簡(jiǎn)單來(lái)說(shuō),Tomcat會(huì)使用java.io.InputStreamjava.io.OutputStream(傳統(tǒng)IO流)將multipart請(qǐng)求中的表單參數(shù)和文件保存到服務(wù)器本地臨時(shí)文件,然后將本地臨時(shí)文件信息封裝成Part對(duì)象返回。
也就是說(shuō),我們?cè)跇I(yè)務(wù)中獲取到的文件實(shí)際上都來(lái)自服務(wù)器本地臨時(shí)文件。

3.4.2 Undertow實(shí)現(xiàn)

為了使用Undertow服務(wù)器,需要引入如下依賴(lài):

<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解析文件請(qǐng)求的核心在于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、解析文件請(qǐng)求,封裝FormData對(duì)象
    FormData formData = parseFormData();  
    // 2、封裝Part對(duì)象
    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;

核心步驟如下:

  • 解析文件請(qǐng)求,封裝FormData對(duì)象
  • 封裝Part對(duì)象
    io.undertow.servlet.spec.HttpServletRequestImpl#parseFormData方法會(huì)進(jìn)行實(shí)際解析文件請(qǐng)求,核心代碼如下:
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);

在這個(gè)過(guò)程中,Undertow會(huì)使用java.io.InputStreamjava.io.OutputStream(傳統(tǒng)IO流),結(jié)合java.nio.ByteBuffermultipart請(qǐng)求中的表單參數(shù)和文件保存到服務(wù)器本地臨時(shí)文件,然后將本地臨時(shí)文件信息封裝成Part對(duì)象返回(具體細(xì)節(jié)可以繼續(xù)深入閱讀相關(guān)源碼)。
也就是說(shuō),我們?cè)跇I(yè)務(wù)中獲取到的文件實(shí)際上都來(lái)自服務(wù)器本地臨時(shí)文件。

3.4.2 Jetty實(shí)現(xiàn)

為了使用Jetty服務(wù)器,需要引入如下依賴(lài):

<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解析文件請(qǐng)求的核心在于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會(huì)創(chuàng)建文件解析器:

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則會(huì)進(jìn)行文件請(qǐng)求解析:

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

在這個(gè)過(guò)程中,Jetty會(huì)使用java.io.InputStreamjava.io.OutputStream(傳統(tǒng)IO流),結(jié)合java.nio.ByteBuffermultipart請(qǐng)求中的表單參數(shù)和文件保存到服務(wù)器本地臨時(shí)文件,然后將本地臨時(shí)文件信息封裝成Part對(duì)象返回。
也就是說(shuō),我們?cè)跇I(yè)務(wù)中獲取到的文件實(shí)際上都來(lái)自服務(wù)器本地臨時(shí)文件。

3.5 StandardServletMultipartResolver#cleanupMultipart

StandardServletMultipartResolver#cleanupMultipart方法會(huì)將臨時(shí)文件刪除:

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解析器,除了基礎(chǔ)的spring-boot-starter-web,還需要額外引入如下依賴(lài):

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

然后,配置名為multipartResolver的bean(此時(shí)Spring Boot不會(huì)添加默認(rèn)文件解析器):

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

4.1 CommonsMultipartResolver#isMultipart

CommonsMultipartResolver解析器會(huì)根據(jù)請(qǐng)求方法和請(qǐng)求頭來(lái)判斷文件請(qǐng)求,源碼如下:

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

supportedMethods成員變量表示支持的請(qǐng)求方法,默認(rèn)為null,可以在初始化時(shí)指定。
當(dāng)supportedMethodsnull時(shí),即在默認(rèn)情況下,會(huì)調(diào)用ServletFileUpload.isMultipartContent()方法進(jìn)行判斷。此時(shí)文件請(qǐng)求的滿(mǎn)足條件為:

  • 請(qǐng)求方法為POST
  • 請(qǐng)求頭Content-Type為以multipart/開(kāi)頭
    當(dāng)supportedMethods不為null時(shí),文件請(qǐng)求滿(mǎn)足條件為:
  • 請(qǐng)求方法在supportedMethods列表中
  • 請(qǐng)求頭Content-Type為以multipart/開(kāi)頭

4.2 CommonsMultipartResolver#resolveMultipart

CommonsMultipartResolver在解析文件請(qǐng)求時(shí),會(huì)將原始請(qǐng)求封裝成DefaultMultipartHttpServletRequest對(duì)象:

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成員變量也表示是否會(huì)馬上解析文件。
當(dāng)resolveLazilyfalse時(shí),即默認(rèn)情況下,不會(huì)立即解析文件,只是會(huì)將原始請(qǐng)求進(jìn)行簡(jiǎn)單封裝。只有在調(diào)用DefaultMultipartHttpServletRequest#getXxx方法時(shí),會(huì)判斷文件是否已經(jīng)解析。如果沒(méi)有解析,會(huì)調(diào)用DefaultMultipartHttpServletRequest#initializeMultipart進(jìn)行解析。
當(dāng)resolveLazilytrue時(shí),會(huì)立即調(diào)用CommonsMultipartResolver#parseRequest方法進(jìn)行文件解析。

4.3 CommonsMultipartResolver#parseRequest

CommonsMultipartResolver#parseRequest方法會(huì)進(jìn)行文件請(qǐng)求解析,總的來(lái)說(shuō)包括兩個(gè)步驟:

  • 解析文件請(qǐng)求
  • 封裝響應(yīng)
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);  
return parseFileItems(fileItems, encoding);

深入閱讀源碼可以發(fā)現(xiàn),在解析文件請(qǐng)求時(shí),會(huì)采用與StandardServletMultipartResolver+Tomcat相同的方式保存臨時(shí)文件:

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方法會(huì)將臨時(shí)文件刪除:

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

到此這篇關(guān)于Spring MVC文件請(qǐng)求處理MultipartResolver詳解的文章就介紹到這了,更多相關(guān)Spring MVC文件請(qǐng)求內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Java中的BlockingQueue阻塞隊(duì)列原理以及實(shí)現(xiàn)詳解

    Java中的BlockingQueue阻塞隊(duì)列原理以及實(shí)現(xiàn)詳解

    這篇文章主要介紹了Java中的BlockingQueue阻塞隊(duì)列原理以及實(shí)現(xiàn)詳解,在最常見(jiàn)的使用到這個(gè)阻塞隊(duì)列的地方,就是我們耳熟能詳?shù)木€程池里面了,作為我們線程池的一大最大參與者,也是AQS的一個(gè)具體實(shí)現(xiàn),需要的朋友可以參考下
    2023-12-12
  • Spring Cloud實(shí)現(xiàn)提供API給客戶(hù)端的方法詳解

    Spring Cloud實(shí)現(xiàn)提供API給客戶(hù)端的方法詳解

    這篇文章主要給大家介紹了關(guān)于Spring Cloud實(shí)現(xiàn)提供API給客戶(hù)端的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2018-01-01
  • java實(shí)現(xiàn)導(dǎo)出Excel的功能

    java實(shí)現(xiàn)導(dǎo)出Excel的功能

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)導(dǎo)出Excel的功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-05-05
  • 關(guān)于SpringBoot的熱部署方案

    關(guān)于SpringBoot的熱部署方案

    這篇文章主要介紹了關(guān)于SpringBoot的熱部署方案,每次修改代碼就得將項(xiàng)目重啟,重新部署,對(duì)于一些大型應(yīng)用來(lái)說(shuō),重啟時(shí)間需要花費(fèi)大量的時(shí)間成本,本文就來(lái)詳解熱部署方案,需要的朋友可以參考下
    2023-05-05
  • java基礎(chǔ)學(xué)習(xí)筆記之泛型

    java基礎(chǔ)學(xué)習(xí)筆記之泛型

    所謂泛型,就是變量類(lèi)型的參數(shù)化。泛型是JDK1.5中一個(gè)最重要的特征。通過(guò)引入泛型,我們將獲得編譯時(shí)類(lèi)型的安全和運(yùn)行時(shí)更小的拋出ClassCastException的可能。在JDK1.5中,你可以聲明一個(gè)集合將接收/返回的對(duì)象的類(lèi)型。
    2016-02-02
  • Spring Boot 驗(yàn)證碼框架 CAPTCHA詳解

    Spring Boot 驗(yàn)證碼框架 CAPTCHA詳解

    這篇文章主要介紹了Spring Boot 驗(yàn)證碼框架 CAPTCHA詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-03-03
  • JAVA讀取二進(jìn)制文件以及畫(huà)圖教程

    JAVA讀取二進(jìn)制文件以及畫(huà)圖教程

    由于項(xiàng)目需要,需要對(duì)二進(jìn)制文件進(jìn)行讀取,所以這篇文章主要給大家介紹了關(guān)于JAVA讀取二進(jìn)制文件以及畫(huà)圖的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-07-07
  • 關(guān)于工廠方法模式的Java實(shí)現(xiàn)

    關(guān)于工廠方法模式的Java實(shí)現(xiàn)

    這篇文章主要介紹了關(guān)于工廠方法模式的Java實(shí)現(xiàn)講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • Java獲取兩個(gè)集合List的交集、補(bǔ)集、并集(相加)和差集(相減)的不同方式

    Java獲取兩個(gè)集合List的交集、補(bǔ)集、并集(相加)和差集(相減)的不同方式

    這篇文章主要給大家介紹了關(guān)于Java獲取兩個(gè)集合List的交集、補(bǔ)集、并集(相加)和差集(相減)的不同方式,在一般操作中對(duì)于list集合取交集、差集、并集,比較簡(jiǎn)單,需要的朋友可以參考下
    2023-08-08
  • IntelliJ IDEA查看方法說(shuō)明文檔的圖解

    IntelliJ IDEA查看方法說(shuō)明文檔的圖解

    今天小編就為大家分享一篇關(guān)于IntelliJ IDEA查看方法說(shuō)明文檔的圖解,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧
    2018-10-10

最新評(píng)論