2023-03-14
原文作者:lifullmoon 原文地址:https://www.cnblogs.com/lifullmoon

MultipartResolver 组件,内容类型( Content-Type )为 multipart/* 的请求的解析器,主要解析文件上传的请求。例如,MultipartResolver 会将 HttpServletRequest 封装成 MultipartHttpServletRequest 对象,便于获取参数信息以及上传的文件

使用方式,可以参考《MyBatis 使用手册》中的 集成 Spring 模块下的 spring-mvc.xml 文件中配置 MultipartResolverCommonsMultipartResolver 实现类,然后在方法入参中用 MultipartFile 类型接收

关于在 SpringBoot 中如何使用文件上传可参考 Spring 官方文档

回顾

先来回顾一下在 DispatcherServlet 中处理请求的过程中哪里使用到 MultipartResolver 组件,可以回到 《一个请求的旅行过程》 中的 DispatcherServletdoDispatch 方法中看看,如下:

    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // ... 省略相关代码
        // <2> 检测请求是否为上传请求,如果是则通过 multipartResolver 将其封装成 MultipartHttpServletRequest 对象
        processedRequest = checkMultipart(request);
        // ... 省略相关代码
    }
    
    protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
        // 如果该请求是一个涉及到 multipart (文件)的请求
        if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
            if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
                if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
                    logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
                }
            }
            else if (hasMultipartException(request)) {
                logger.debug("Multipart resolution previously failed for current request - " +
                        "skipping re-resolution for undisturbed error rendering");
            }
            else {
                try {
                    // 将 HttpServletRequest 请求封装成 MultipartHttpServletRequest 对象,解析请求里面的参数以及文件
                    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;
    }

<2> 处,如果该请求是一个涉及到 multipart (文件)的请求,则通过 multipartResolverHttpServletRequest 请求封装成 MultipartHttpServletRequest 对象,解析请求里面的参数以及文件

MultipartResolver接口

org.springframework.web.multipart.MultipartResolver 接口,内容类型( Content-Type )为 multipart/* 的请求的解析器接口,代码如下:

    public interface MultipartResolver {
    	/**
    	 * 是否为 multipart 请求
    	 */
    	boolean isMultipart(HttpServletRequest request);
    	/**
    	 * 将 HttpServletRequest 请求封装成 MultipartHttpServletRequest 对象
    	 */
    	MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
    
    	/**
    	 * 清理处理 multipart 产生的资源,例如临时文件
    	 */
    	void cleanupMultipart(MultipartHttpServletRequest request);
    }

MultipartResolver 接口体系的结构如下:

202303142234100081.png

一共有两块:

  • 上半部分,MultipartRequest 接口及其实现类
  • 下半部分,MultipartResolver 接口以及其实现类

初始化过程

DispatcherServletinitMultipartResolver(ApplicationContext context) 方法,初始化 MultipartResolver 组件,方法如下:

    private void initMultipartResolver(ApplicationContext context) {
        try {
            // 从 Spring 上下文中获取名称为 "multipartResolver" ,类型为 MultipartResolver 的 Bean
            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 MVC 中,multipartResolver 默认为 null 【注意】 ,需要自己配置,例如《MyBatis 使用手册》中的 集成 Spring 模块下的 spring-mvc.xml 文件中配置 MultipartResolver 为 CommonsMultipartResolver 实现类,也可以配置为 StandardServletMultipartResolver 实现类
  • 在 Spring Boot 中,multipartResolver 默认为 StandardServletMultipartResolver 实现类

目前 Spring 只提供上面两种实现类,接下来依次进行分析

StandardServletMultipartResolver

org.springframework.web.multipart.support.StandardServletMultipartResolver,实现 MultipartResolver 接口,基于 Servlet 3.0 标准的上传文件 API 的 MultipartResolver 实现类,代码如下:

    public class StandardServletMultipartResolver implements MultipartResolver {
    
       /**
        * 是否延迟解析
        */
       private boolean resolveLazily = false;
    
       public void setResolveLazily(boolean resolveLazily) {
          this.resolveLazily = resolveLazily;
       }
    
    
       @Override
       public boolean isMultipart(HttpServletRequest request) {
          // 请求的 Content-type 必须 multipart/ 开头
          return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
       }
    
       @Override
       public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
          return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
       }
    
       @Override
       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 {
                // 删除临时的 Part
                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);
             }
          }
       }
    
    }
  • isMultipart(HttpServletRequest request)方法,请求的 Content-type 是否以 multipart/ 开头
  • resolveMultipart(HttpServletRequest request)方法,直接将 HttpServletRequest 转换成 StandardMultipartHttpServletRequest 对象
  • cleanupMultipart(MultipartHttpServletRequest request)方法,清理资源,删除临时的 javax.servlet.http.Part

StandardMultipartHttpServletRequest

org.springframework.web.multipart.support.StandardMultipartHttpServletRequest,继承 AbstractMultipartHttpServletRequest 抽象类,基于 Servlet 3.0 的 Multipart HttpServletRequest 实现类,包含了一个 javax.servlet.http.HttpServletRequest 对象和它的 javax.servlet.http.Part 对象们,其中 Part 对象会被封装成 StandardMultipartFile 对象

构造方法

    public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
    	/**
    	 * 普通参数名的集合
    	 */
    	@Nullable
    	private Set<String> multipartParameterNames;
    
    	public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
    		this(request, false);
    	}
    
    	public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
    		super(request);
    		// 如果不需要延迟解析
    		if (!lazyParsing) {
    			// 解析请求
    			parseRequest(request);
    		}
    	}
    }
  • multipartParameterNames:普通参数名的集合,非上传文件的参数名
  • 如果不需要延迟解析,则调用 parseRequest(HttpServletRequest request) 方法,直接解析请求

parseRequest

parseRequest(HttpServletRequest request) 方法,解析请求,解析 HttpServletRequest 中的 Part 对象,如果是文件,则封装成 StandardMultipartFile 对象,否则就是普通参数,获取其名称,如下:

    private void parseRequest(HttpServletRequest request) {
        try {
            // <1> 从 HttpServletRequest 中获取 Part 们
            Collection<Part> parts = request.getParts();
            this.multipartParameterNames = new LinkedHashSet<>(parts.size());
            MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
            // <2> 遍历 parts 数组
            for (Part part : parts) {
                // <2.1> 获得请求头中的 Content-Disposition 信息,MIME 协议的扩展
                String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
                // <2.2> 对 Content-Disposition 信息进行解析,生成 ContentDisposition 对象
                // 包含请求参数信息,以面向“对象”的形式进行访问
                ContentDisposition disposition = ContentDisposition.parse(headerValue);
                // <2.3> 获得文件名
                String filename = disposition.getFilename();
                // <2.4> 情况一,文件名非空,说明是文件参数,则创建 StandardMultipartFile 对象
                if (filename != null) {
                    if (filename.startsWith("=?") && filename.endsWith("?=")) {
                        filename = MimeDelegate.decode(filename);
                    }
                    files.add(part.getName(), new StandardMultipartFile(part, filename));
                }
                // <2.5> 情况二,文件名为空,说明是普通参数,则保存参数名称
                else {
                    this.multipartParameterNames.add(part.getName());
                }
            }
            // <3> 将上面生成的 StandardMultipartFile 文件对象们,设置到父类的 multipartFiles 属性中
            setMultipartFiles(files);
        }
        catch (Throwable ex) {
            handleParseFailure(ex);
        }
    }
  1. 从 HttpServletRequest 中获取 Part 们

  2. 遍历 parts 数组

    1. 从 Part 对象中获得请求头中的 Content-Disposition 信息,MIME 协议的扩展
    2. 对 Content-Disposition 信息进行解析,生成 ContentDisposition 对象,包含请求参数信息,以面向“对象”的形式进行访问
    3. ContentDisposition 对象中获得文件名
    4. 情况一,文件名非空,说明是文件参数,则创建 StandardMultipartFile 对象
    5. 情况二,文件名为空,说明是普通参数,则保存参数名称
  3. 将上面生成的 StandardMultipartFile 文件对象们,设置到父类的 multipartFiles 属性中

  4. 如果发生异常则抛出

其他方法

    /** 初始化请求 */
    @Override
    protected void initializeMultipart() {
        parseRequest(getRequest());
    }
    /** 获取请求中的参数名称 */
    @Override
    public Enumeration<String> getParameterNames() {
        if (this.multipartParameterNames == null) {
            initializeMultipart();
        }
        if (this.multipartParameterNames.isEmpty()) {
            return super.getParameterNames();
        }
    
        // Servlet 3.0 getParameterNames() not guaranteed to include multipart form items
        // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
        Set<String> paramNames = new LinkedHashSet<>();
        Enumeration<String> paramEnum = super.getParameterNames();
        while (paramEnum.hasMoreElements()) {
            paramNames.add(paramEnum.nextElement());
        }
        paramNames.addAll(this.multipartParameterNames);
        return Collections.enumeration(paramNames);
    }
    /** 获取请求中的参数,参数名和参数值的映射 */
    @Override
    public Map<String, String[]> getParameterMap() {
        if (this.multipartParameterNames == null) {
            initializeMultipart();
        }
        if (this.multipartParameterNames.isEmpty()) {
            return super.getParameterMap();
        }
        // Servlet 3.0 getParameterMap() not guaranteed to include multipart form items
        // (e.g. on WebLogic 12) -> need to merge them here to be on the safe side
        Map<String, String[]> paramMap = new LinkedHashMap<>(super.getParameterMap());
        for (String paramName : this.multipartParameterNames) {
            if (!paramMap.containsKey(paramName)) {
                paramMap.put(paramName, getParameterValues(paramName));
            }
        }
        return paramMap;
    }
    /** 获取请求的 Content-Type 内容类型 */
    @Override
    public String getMultipartContentType(String paramOrFileName) {
        try {
            Part part = getPart(paramOrFileName);
            return (part != null ? part.getContentType() : null);
        }
        catch (Throwable ex) {
            throw new MultipartException("Could not access multipart servlet request", ex);
        }
    }
    /** 获取请求头信息 */
    @Override
    public HttpHeaders getMultipartHeaders(String paramOrFileName) {
        try {
            Part part = getPart(paramOrFileName);
            if (part != null) {
                HttpHeaders headers = new HttpHeaders();
                for (String headerName : part.getHeaderNames()) {
                    headers.put(headerName, new ArrayList<>(part.getHeaders(headerName)));
                }
                return headers;
            }
            else {
                return null;
            }
        }
        catch (Throwable ex) {
            throw new MultipartException("Could not access multipart servlet request", ex);
        }
    }

StandardMultipartFile

org.springframework.web.multipart.support.StandardMultipartHttpServletRequest 的私有内部静态类,实现了 MultipartFile 接口和 Serializable 接口,内部封装了 javax.servlet.http.Part 对象和文件名称,代码如下:

    private static class StandardMultipartFile implements MultipartFile, Serializable {
    
        private final Part part;
    
        private final String filename;
    
        public StandardMultipartFile(Part part, String filename) {
            this.part = part;
            this.filename = filename;
        }
    
        @Override
        public String getName() {
            return this.part.getName();
        }
    
        @Override
        public String getOriginalFilename() {
            return this.filename;
        }
    
        @Override
        public String getContentType() {
            return this.part.getContentType();
        }
    
        @Override
        public boolean isEmpty() {
            return (this.part.getSize() == 0);
        }
    
        @Override
        public long getSize() {
            return this.part.getSize();
        }
    
        @Override
        public byte[] getBytes() throws IOException {
            return FileCopyUtils.copyToByteArray(this.part.getInputStream());
        }
    
        @Override
        public InputStream getInputStream() throws IOException {
            return this.part.getInputStream();
        }
    
        @Override
        public void transferTo(File dest) throws IOException, IllegalStateException {
            this.part.write(dest.getPath());
            if (dest.isAbsolute() && !dest.exists()) {
                // Servlet 3.0 Part.write is not guaranteed to support absolute file paths:
                // may translate the given path to a relative location within a temp dir
                // (e.g. on Jetty whereas Tomcat and Undertow detect absolute paths).
                // At least we offloaded the file from memory storage; it'll get deleted
                // from the temp dir eventually in any case. And for our user's purposes,
                // we can manually copy it to the requested location as a fallback.
                FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath()));
            }
        }
    
        @Override
        public void transferTo(Path dest) throws IOException, IllegalStateException {
            FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));
        }
    }

这个类封装了 Servlet 3.0 的 Part 对象,也就是我们常用到的 MultipartFile 对象,支持对文件的操作,内部其实都是调用 javax.servlet.http.Part 的方法

AbstractMultipartHttpServletRequest

org.springframework.web.multipart.support.AbstractMultipartHttpServletRequest 抽象类,继承了 HttpServletRequestWrapper 类,实现了 MultipartHttpServletRequest接口

该类是 StandardMultipartHttpServletRequestDefaultMultipartHttpServletRequest 的父类,实现了一些公共的方法,代码如下:

    public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper implements MultipartHttpServletRequest {
        /**
         * 请求中的文件信息
         */
    	@Nullable
    	private MultiValueMap<String, MultipartFile> multipartFiles;
    
    	protected AbstractMultipartHttpServletRequest(HttpServletRequest request) {
    		super(request);
    	}
    
    	@Override
    	public HttpServletRequest getRequest() {
    		return (HttpServletRequest) super.getRequest();
    	}
    
    	@Override
    	public HttpMethod getRequestMethod() {
    		return HttpMethod.resolve(getRequest().getMethod());
    	}
    
        /** 获取请求头信息 */
    	@Override
    	public HttpHeaders getRequestHeaders() {
    		HttpHeaders headers = new HttpHeaders();
    		Enumeration<String> headerNames = getHeaderNames();
    		while (headerNames.hasMoreElements()) {
    			String headerName = headerNames.nextElement();
    			headers.put(headerName, Collections.list(getHeaders(headerName)));
    		}
    		return headers;
    	}
    
        /** 获取文件名称列表 */
    	@Override
    	public Iterator<String> getFileNames() {
    		return getMultipartFiles().keySet().iterator();
    	}
    
        /** 获取指定文件名的单个文件 */
    	@Override
    	public MultipartFile getFile(String name) {
    		return getMultipartFiles().getFirst(name);
    	}
    
        /** 获取指定文件名的多个文件 */
    	@Override
    	public List<MultipartFile> getFiles(String name) {
    		List<MultipartFile> multipartFiles = getMultipartFiles().get(name);
    		if (multipartFiles != null) {
    			return multipartFiles;
    		}
    		else {
    			return Collections.emptyList();
    		}
    	}
    
    	@Override
    	public Map<String, MultipartFile> getFileMap() {
    		return getMultipartFiles().toSingleValueMap();
    	}
    
    	@Override
    	public MultiValueMap<String, MultipartFile> getMultiFileMap() {
    		return getMultipartFiles();
    	}
    
    	public boolean isResolved() {
    		return (this.multipartFiles != null);
    	}
    
    	protected final void setMultipartFiles(MultiValueMap<String, MultipartFile> multipartFiles) {
    		this.multipartFiles = new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles));
    	}
    
    	protected MultiValueMap<String, MultipartFile> getMultipartFiles() {
    		if (this.multipartFiles == null) {
    			initializeMultipart();
    		}
    		return this.multipartFiles;
    	}
    
    	/** 交由子类实现 */
    	protected void initializeMultipart() {
    		throw new IllegalStateException("Multipart request not initialized");
    	}
    }

上面的方法都比较简单,用于获取请求中的文件对象

MultiValueMap<String, MultipartFile> multipartFiles属性,保存由子类解析出请求中的 Part 对象所封装成的 MultipartFile 对象

CommonsMultipartResolver

org.springframework.web.multipart.commons.CommonsMultipartResolver,实现 MultipartResolver、ServletContextAware 接口,继承 CommonsFileUploadSupport 抽象类,基于 Apache Commons FileUpload 的 MultipartResolver 实现类

如果需要使用这个 MultipartResolver 实现类,需要引入 commons-fileuploadcommons-iocommons-codec 组件,例如:

    <dependencies>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.4</version>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.15</version>
        </dependency>
    </dependencies>

注意 ,如果 Spring Boot 项目中需要使用 CommonsMultipartResolver,需要在 application.yml 中添加如下配置,排除其默认的配置,如下:

    spring:
      autoconfigure:
        exclude: org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration

构造方法

    public class CommonsMultipartResolver extends CommonsFileUploadSupport implements MultipartResolver, ServletContextAware {
    	/**
    	 * 是否延迟解析
    	 */
    	private boolean resolveLazily = false;
    
    	public CommonsMultipartResolver() {
    		super();
    	}
    
    	public CommonsMultipartResolver(ServletContext servletContext) {
    		this();
    		setServletContext(servletContext);
    	}
    }

isMultipart

    @Override
    public boolean isMultipart(HttpServletRequest request) {
        // 必须是 POST 请求,且 Content-Type 为 multipart/ 开头
        return ServletFileUpload.isMultipartContent(request);
    }

判断是否为 multipart 请求,必须是 POST 请求,且 Content-Type 为 multipart/ 开头

resolveMultipart

    @Override
    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());
        }
    }

将 HttpServletRequest 转换成 DefaultMultipartHttpServletRequest 对象

如果开启了延迟解析,则重写该对象的 initializeMultipart() 方法,用于解析请求

否则直接调用 parseRequest(HttpServletRequest request) 方法解析请求,返回 MultipartParsingResult 对象,包含 MultipartFile 对象和普通参数信息

parseRequest

parseRequest(HttpServletRequest request)方法,用于解析请求,返回 MultipartParsingResult 对象,包含 MultipartFile 对象、普通参数信息以及参数的 Content-Type 信息,方法如下:

    protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
        // <1> 获取请求中的编码
        String encoding = determineEncoding(request);
        // <2> 获取 ServletFileUpload 对象
        FileUpload fileUpload = prepareFileUpload(encoding);
        try {
            // <3> 获取请求中的流数据
            List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
            // <4> 将这些流数据转换成 MultipartParsingResult,包含 CommonsMultipartFile、参数信息、Content-type
            return parseFileItems(fileItems, encoding);
        }
        catch (FileUploadBase.SizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
        }
        catch (FileUploadBase.FileSizeLimitExceededException ex) {
            throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
        }
        catch (FileUploadException ex) {
            throw new MultipartException("Failed to parse multipart servlet request", ex);
        }
    }
  1. 获取请求中的编码
  2. 根据编码获取到 ServletFileUpload 对象( commons-fileupload 中的类),在 newFileUpload(FileItemFactory fileItemFactory) 方法中返回的就是 ServletFileUpload 对象,可以看到父类 CommonsFileUploadSupport 的构造方法,如下:
        // org.springframework.web.multipart.commons.CommonsFileUploadSupport.java
        
        public CommonsFileUploadSupport() {
            this.fileItemFactory = newFileItemFactory();
            // 由子类实现
            this.fileUpload = newFileUpload(getFileItemFactory());
        }
具体细节就不讲述了
  1. 通过 ServletFileUpload 对象解析请求,返回流数据 List<FileItem> fileItems
  2. 调用父类 CommonsFileUploadSupport 的 parseFileItems(List<FileItem> fileItems, String encoding) 方法,将这些流数据转换成 MultipartParsingResult 对象
        // org.springframework.web.multipart.commons.CommonsFileUploadSupport.java
        
        protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
            MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<>();
            Map<String, String[]> multipartParameters = new HashMap<>();
            Map<String, String> multipartParameterContentTypes = new HashMap<>();
        
            // Extract multipart files and multipart parameters.
            for (FileItem fileItem : fileItems) {
                if (fileItem.isFormField()) {
                    String value;
                    String partEncoding = determineEncoding(fileItem.getContentType(), encoding);
                    try {
                        value = fileItem.getString(partEncoding);
                    }
                    catch (UnsupportedEncodingException ex) {
                        if (logger.isWarnEnabled()) {
                            logger.warn("Could not decode multipart item '" + fileItem.getFieldName() +
                                    "' with encoding '" + partEncoding + "': using platform default");
                        }
                        value = fileItem.getString();
                    }
                    String[] curParam = multipartParameters.get(fileItem.getFieldName());
                    if (curParam == null) {
                        // simple form field
                        multipartParameters.put(fileItem.getFieldName(), new String[] {value});
                    }
                    else {
                        // array of simple form fields
                        String[] newParam = StringUtils.addStringToArray(curParam, value);
                        multipartParameters.put(fileItem.getFieldName(), newParam);
                    }
                    multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());
                }
                else {
                    // multipart file field
                    CommonsMultipartFile file = createMultipartFile(fileItem);
                    multipartFiles.add(file.getName(), file);
                    LogFormatUtils.traceDebug(logger, traceOn ->
                            "Part '" + file.getName() + "', size " + file.getSize() +
                                    " bytes, filename='" + file.getOriginalFilename() + "'" +
                                    (traceOn ? ", storage=" + file.getStorageDescription() : "")
                    );
                }
            }
            return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
        }
大致就是遍历 `fileItems` 集合,如果是一个简单的表单字段,那么就是一个普通的参数,将参数名和值保存起来

否则就是文件,将其封装成 `CommonsMultipartFile` 保存起来

cleanupMultipart

cleanupMultipart(MultipartHttpServletRequest request)方法,清理文件产生的临时资源,如下:

    // CommonsMultipartResolver.java
    @Override
    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);
            }
        }
    }
    // CommonsFileUploadSupport.java
    protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) {
        for (List<MultipartFile> files : multipartFiles.values()) {
            for (MultipartFile file : files) {
                if (file instanceof CommonsMultipartFile) {
                    CommonsMultipartFile cmf = (CommonsMultipartFile) file;
                    cmf.getFileItem().delete();
                    LogFormatUtils.traceDebug(logger, traceOn -> "Cleaning up part '..."));
                }
            }
        }
    }

DefaultMultipartHttpServletRequest

org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest,继承 AbstractMultipartHttpServletRequest 抽象类,MultipartHttpServletRequest 的默认实现类,代码如下:

    public class DefaultMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
    
    	private static final String CONTENT_TYPE = "Content-Type";
    
    	@Nullable
    	private Map<String, String[]> multipartParameters;
    
    	@Nullable
    	private Map<String, String> multipartParameterContentTypes;
    
    	public DefaultMultipartHttpServletRequest(HttpServletRequest request, MultiValueMap<String, MultipartFile> mpFiles,
    			Map<String, String[]> mpParams, Map<String, String> mpParamContentTypes) {
    
    		super(request);
    		setMultipartFiles(mpFiles);
    		setMultipartParameters(mpParams);
    		setMultipartParameterContentTypes(mpParamContentTypes);
    	}
    
    	public DefaultMultipartHttpServletRequest(HttpServletRequest request) {
    		super(request);
    	}
    
    	@Override
    	@Nullable
    	public String getParameter(String name) {
    		String[] values = getMultipartParameters().get(name);
    		if (values != null) {
    			return (values.length > 0 ? values[0] : null);
    		}
    		return super.getParameter(name);
    	}
    
    	@Override
    	public String[] getParameterValues(String name) {
    		String[] parameterValues = super.getParameterValues(name);
    		String[] mpValues = getMultipartParameters().get(name);
    		if (mpValues == null) {
    			return parameterValues;
    		}
    		if (parameterValues == null || getQueryString() == null) {
    			return mpValues;
    		}
    		else {
    			String[] result = new String[mpValues.length + parameterValues.length];
    			System.arraycopy(mpValues, 0, result, 0, mpValues.length);
    			System.arraycopy(parameterValues, 0, result, mpValues.length, parameterValues.length);
    			return result;
    		}
    	}
    
    	@Override
    	public Enumeration<String> getParameterNames() {
    		Map<String, String[]> multipartParameters = getMultipartParameters();
    		if (multipartParameters.isEmpty()) {
    			return super.getParameterNames();
    		}
    
    		Set<String> paramNames = new LinkedHashSet<>();
    		paramNames.addAll(Collections.list(super.getParameterNames()));
    		paramNames.addAll(multipartParameters.keySet());
    		return Collections.enumeration(paramNames);
    	}
    
    	@Override
    	public Map<String, String[]> getParameterMap() {
    		Map<String, String[]> result = new LinkedHashMap<>();
    		Enumeration<String> names = getParameterNames();
    		while (names.hasMoreElements()) {
    			String name = names.nextElement();
    			result.put(name, getParameterValues(name));
    		}
    		return result;
    	}
    
    	@Override
    	public String getMultipartContentType(String paramOrFileName) {
    		MultipartFile file = getFile(paramOrFileName);
    		if (file != null) {
    			return file.getContentType();
    		}
    		else {
    			return getMultipartParameterContentTypes().get(paramOrFileName);
    		}
    	}
    
    	@Override
    	public HttpHeaders getMultipartHeaders(String paramOrFileName) {
    		String contentType = getMultipartContentType(paramOrFileName);
    		if (contentType != null) {
    			HttpHeaders headers = new HttpHeaders();
    			headers.add(CONTENT_TYPE, contentType);
    			return headers;
    		}
    		else {
    			return null;
    		}
    	}
    
    	protected final void setMultipartParameters(Map<String, String[]> multipartParameters) {
    		this.multipartParameters = multipartParameters;
    	}
    
    	protected Map<String, String[]> getMultipartParameters() {
    		if (this.multipartParameters == null) {
    			initializeMultipart();
    		}
    		return this.multipartParameters;
    	}
    
    	protected final void setMultipartParameterContentTypes(Map<String, String> multipartParameterContentTypes) {
    		this.multipartParameterContentTypes = multipartParameterContentTypes;
    	}
    
    	protected Map<String, String> getMultipartParameterContentTypes() {
    		if (this.multipartParameterContentTypes == null) {
    			initializeMultipart();
    		}
    		return this.multipartParameterContentTypes;
    	}
    }

代码并不复杂,稍微阅读一下就理解了

总结

本文对 Spring MVC 处理请求的过程中使用到的 MultipartResolver 组件进行了分析,如果请求的 Content-Typemultipart/*,涉及到文件上传,所以处理请求的第一步需要通过 MultipartResolver 组件对请求进行转换处理。会将 HttpServletRequest 请求对象封装成 MultipartHttpServletRequest 对象,便于获取参数信息和操作上传的文件(MultipartFile 对象)。

MultipartResolver 组件的实现类有两种:

  • org.springframework.web.multipart.support.StandardServletMultipartResolver:实现 MultipartResolver 接口,基于 Servlet 3.0 标准的上传文件 API 的 MultipartResolver 实现类
  • org.springframework.web.multipart.commons.CommonsMultipartResolver:实现 MultipartResolver 接口,基于 Apache Commons FileUpload 的 MultipartResolver 实现类

两者的区别:

  • StandardServletMultipartResolver 会将 HttpServletRequest 封装成 StandardMultipartHttpServletRequest 对象,由 Servlet 3.0 提供 API 获取请求中的 javax.servlet.http.Part 对象,然后进行解析,文件会封装成 StandardMultipartFile 对象
  • CommonsMultipartResolver 会将 HttpServletRequest 封装成 DefaultMultipartHttpServletRequest 对象,由 Apache 的 Commons FileUpload 组件来实现,通过 org.apache.commons.fileupload.servlet.ServletFileUpload 对象获取请求中的 org.apache.commons.fileupload.FileItem 对象,然后进行解析,文件会封装成 CommonsMultipartFile 对象,如何使用可以参考上面的 CommonsMultipartResolver 小节

注意事项:

  • 在 Spring MVC 中,multipartResolver 默认为 null,需要自己配置,例如《MyBatis 使用手册》中的 集成 Spring 模块下的 spring-mvc.xml 文件中配置 MultipartResolver 为 CommonsMultipartResolver 实现类,也可以配置为 StandardServletMultipartResolver 实现类
  • 在 Spring Boot 中,multipartResolver 默认为 StandardServletMultipartResolver 实现类
阅读全文