• 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. * 

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

    <dependency>  
       <groupId>org.springframework.bootgroupId>  
       <artifactId>spring-boot-starter-webartifactId>  
    dependency>
    

    会默认引入Tomcat依赖:

    <dependency>  
       <groupId>org.springframework.bootgroupId>  
       <artifactId>spring-boot-starter-tomcatartifactId>  
    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 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服务器,需要引入如下依赖:

    <dependency>  
       <groupId>org.springframework.bootgroupId>  
       <artifactId>spring-boot-starter-webartifactId>  
       <exclusions>  
          <exclusion>  
             <groupId>org.springframework.bootgroupId>  
             <artifactId>spring-boot-starter-tomcatartifactId>  
          exclusion>  
       exclusions>  
    dependency>  
    <dependency>  
       <groupId>org.springframework.bootgroupId>  
       <artifactId>spring-boot-starter-undertowartifactId>  
    dependency>
    

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

    <dependency>  
       <groupId>org.springframework.bootgroupId>  
       <artifactId>spring-boot-starter-webartifactId>  
       <exclusions>  
          <exclusion>  
             <groupId>org.springframework.bootgroupId>  
             <artifactId>spring-boot-starter-tomcatartifactId>  
          exclusion>  
       exclusions>  
    dependency>  
    <dependency>  
       <groupId>org.springframework.bootgroupId>  
       <artifactId>spring-boot-starter-jettyartifactId>  
    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 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,还需要额外引入如下依赖:

    <dependency>  
       <groupId>commons-fileuploadgroupId>  
       <artifactId>commons-fileuploadartifactId>  
       <version>1.4version>  
    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时,文件请求满足条件为:

    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);  
          }  
       }  
    }
    
  • 相关阅读:
    头插法 尾插法建立链表(带头节点、不带头节点)
    数据库与我:一段关于学习与成长的深情回顾
    2024年,我又开始用Linux桌面作为主力系统了~
    中国人民大学与加拿大女王大学金融硕士——山有顶峰,海有彼岸,一切终有回甘
    .NET分布式Orleans - 9 - 贪吃蛇项目演示
    KubeEdge-Ianvs v0.2 发布:终身学习支持非结构化场景
    【问题思考总结】NAT的公有地址怎么转换为私有地址?【MAC地址和IP地址的转换】
    【概率论与数理统计(研究生课程)】知识点总结2(一维随机变量及其分布)
    基于JavaSwing开发个人简历程序 课程设计 大作业
    折腾指南: 将光猫改造成你的NAS,WebDAV+网页文件管理器vList5+natmap
  • 原文地址:https://www.cnblogs.com/Xianhuii/p/16933750.html