SpringMVC源码之:MultipartResolver

332 阅读10分钟

1. Servlet的与文件上传相关的API

Servlet 3.0新增了对文件上传的支持,避免了使用commons-fileupload.jar工具包的麻烦

1.1. Part

Part接口用于封装form表单提交的数据;一个Part对象就代表form表单中的一个键值对;示例如下:

/**
 * 要使Servlet支持文件上传,需要在Servlet类上添加@MultipartConfig注解
 * 或者在web.xml文件中配置该Servlet时,在servlet标签内添加multipart-config标签来手动配置
 */
@Slf4j
@MultipartConfig
public class FileUploadServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        
        // 通过请求的getParts()方法获取到所有表单项,然后依次处理这些表单项
        // 也可以通过请求的getPart(name)方法获取到指定名称的表单项
        for (Part part : request.getParts()) {
            
            // 获取到当前表单项的所有头信息
            Map<String, String> headers = new HashMap<>();
            for (String headerName : part.getHeaderNames()) {
                headers.put(headerName, part.getHeader(headerName));
            }

            // 表单项的名称,即参数名称
            log.info("name: {}", part.getName());
            
            // 表单项的内容类型,可以是字符串类型或文件流类型
            log.info("contentType: {}", part.getContentType());
            
            // 字符串或文件的字节大小
            log.info("size: {}", part.getSize());
            
            // 所有头信息
            log.info("headers: {}", headers);
            
            // 文件名称;如果是字符串类型的表单项,则为null
            log.info("fileName: {}", part.getSubmittedFileName());
            
            // 上传文件时,Servlet容器会将文件存放到临时目录
            // 我们可以通过write()方法将上传的文件保存到自己的目录下(并且此时会删除临时文件)
            // 这里的filename参数可以是文件名称,也可以是绝对路径
            // 注意,如果是字符串类型的表单项,则write()方法不会执行写入操作
            // part.write(filename);
            
            // 获取字符串本身或文件内容的输入流
            // part.getInputStream();
            
            // 删除临时文件;如果是字符串类型的表单项,则不会做任何事
            // part.delete();
        }
    }
}

访问该FileUploadServlet,控制台打印结果如下所示:

# 文件类型的表单项
name: file
contentType: text/plain
size: 17
headers: {Content-Disposition=form-data; name="file"; filename="1.txt", Content-Type=text/plain}
fileName: 1.txt

# 字符串类型的表单项
name: key
contentType: null
size: 5
headers: {Content-Disposition=form-data; name="key"}
fileName: null

1.2. @MultipartConfig

/**
 * 该注解用于标记Servlet类,代表目标Servlet实例需要处理multipart/form-data类型的请求
 * 本注解包含了与文件上传相关的配置,比如文件的保存路径、文件的最大大小等
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultipartConfig {

    /**
     * 文件的保存目录;当调用Part的write(filename)方法时,如果filename是相对路径,那么该属性将会发挥作用:
     * 1. 如果指定了该属性,那么文件将会保存到这里指定的目录中
     * 2. 如果没有指定,则默认保存到servletContext.getDeployment().getDeploymentInfo().getTempPath()目录中
     */
    String location() default "";

    /**
     * 上传文件时,文件的最大大小;这里不是指单个文件,而是所有文件加起来不能超过该值;-1代表不限制
     */
    long maxFileSize() default -1L;

    /**
     * form表单请求的最大大小;-1代表不限制
     */
    long maxRequestSize() default -1L;

    /**
     * 保存为临时文件的大小阈值;当上传文件的大小超过该值时,Servlet容器会为上传的文件创建临时文件,否则文件内容直接保存在内存中
     */
    int fileSizeThreshold() default 0;
}

2. MultipartFile

2.1. 概述

/**
 * MultipartFile是SpringMVC提供的、用于表示用户所上传的文件的接口
 * 上传的文件的内容可以保存在内存中或临时文件中;如果保存在临时文件中,那么当请求处理完成时,需要清除临时文件
 */
public interface MultipartFile extends InputStreamSource {

    /**
     * 获取表单项的参数名称,而不是文件名称
     */
    String getName();

    /**
     * 获取上传的文件在客户端的文件系统中的原始文件名称(一般都会包含路径信息,除了Opera浏览器)
     * 
     * 需要注意的是,这里的文件名是由客户端提供的,因此不应该被盲目地使用,因为:
     * 1. 一方面,不要使用文件名称中的目录部分
     * 2. 另一方面,文件名部分也有可能包含".."之类的恶意字符
     * 
     * 如果返回空串,则代表没有上传文件;如果返回null,则代表未指定或获取不到原始名称
     */
    @Nullable
    String getOriginalFilename();

    /**
     * 获取文件的内容类型
     * 如果返回null,则代表没有上传文件或未指定内容类型
     */
    @Nullable
    String getContentType();

    /**
     * 判断是否有上传的内容
     * 如果未上传文件,或者上传的是空文件,则返回true
     */
    boolean isEmpty();

    /**
     * 获取文件内容的字节大小
     * 如果未上传文件,或者上传的是空文件,则返回0
     */
    long getSize();

    /**
     * 获取文件内容
     */
    byte[] getBytes() throws IOException;

    /**
     * 获取文件内容的输入流;用户需要自己关闭该流
     */
    @Override
    InputStream getInputStream() throws IOException;

    /**
     * 将上传的文件封装成Resource对象,方便RestTemplate和WebClient获取文件名称、内容长度和文件输入流
     */
    default Resource getResource() {
        return new MultipartFileResource(this);
    }

    /**
     * 将上传的文件写入到目标文件中;如果目标文件已存在,则先删除
     * 子类实现该方法时,可以对临时文件进行剪切或复制,也可以将内存中的文件内容写入到目标文件
     * 由于这里没有限制不能剪切,因此该方法最好只调用一次,确保不会出现文件剪切后找不到的情况
     */
    void transferTo(File dest) throws IOException, IllegalStateException;

    /**
     * 将上传的文件写入到目标文件中;如果目标文件已存在,则先删除
      */
    default void transferTo(Path dest) throws IOException, IllegalStateException {
        FileCopyUtils.copy(getInputStream(), Files.newOutputStream(dest));
    }
}

2.2. StandardMultipartFile

/**
 * 本类是StandardMultipartHttpServletRequest的静态内部类,主要负责将Servlet中的Part实例适配成MultipartFile实例
 */
private static class StandardMultipartFile implements MultipartFile, Serializable {
    private final Part part;
    private final String filename;

    /**
     * 全参构造器,需指定Servlet中的Part对象和文件的名称
     */
    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));
    }
}

3. MultipartHttpServletRequest

3.1. 概述

/**
 * MultipartRequest用于代表上传文件的请求,该接口的所有方法都是与文件上传相关的
 */
public interface MultipartRequest {

    /**
     * 获取文件上传的参数集合(如["file"])的迭代器
     * 注意,这里获取的是form表单项的参数名称,而不是真实的文件名称
     */
    Iterator<String> getFileNames();

    /**
     * 获取某个form表单项参数对应的文件信息;如果没有,则返回null
     * 注意,同一个参数可以用于上传多个文件;如果该参数有多个文件,则返回第一个文件
     */
    @Nullable
    MultipartFile getFile(String name);

    /**
     * 获取某个form表单项参数对应的所有文件信息;如果没有,则返回空列表
     */
    List<MultipartFile> getFiles(String name);

    /**
     * 获取form表单项参数及其对应的文件信息的映射关系
     */
    Map<String, MultipartFile> getFileMap();

    /**
     * 获取form表单项参数及其对应的所有文件信息的映射关系
     */
    MultiValueMap<String, MultipartFile> getMultiFileMap();

    /**
     * 获取某个form表单项参数对应的文件的内容类型
     */
    @Nullable
    String getMultipartContentType(String paramOrFileName);
}
/**
 * MultipartHttpServletRequest接口同时继承了HttpServletRequest接口和MultipartRequest接口
 */
public interface MultipartHttpServletRequest extends HttpServletRequest, MultipartRequest {

    /**
     * 获取Http请求方式
     */
    @Nullable
    HttpMethod getRequestMethod();

    /**
     * 获取Http请求头
     */
    HttpHeaders getRequestHeaders();

    /**
     * 获取某个form表单项对应的文件的头部信息
     */
    @Nullable
    HttpHeaders getMultipartHeaders(String paramOrFileName);
}

3.2. AbstractMultipartHttpServletRequest

/**
 * MultipartHttpServletRequest接口的抽象的基本实现类,主要负责管理预生成的MultipartFile实例
 * 本类继承自HttpServletRequestWrapper,是HttpServletRequest的装饰器
 */
public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper
        implements MultipartHttpServletRequest {

    /**
     * 用于缓存此次请求上传的文件信息,其中key为表单项参数名称
     */
    @Nullable
    private MultiValueMap<String, MultipartFile> multipartFiles;

    /**
     * 构造方法,需提供被装饰的HttpServletRequest对象
     */
    protected AbstractMultipartHttpServletRequest(HttpServletRequest request) {
        super(request);
    }

    /**
     * 给this.multipartFiles字段进行赋值;该方法由子类在解析完请求后调用
     */
    protected final void setMultipartFiles(MultiValueMap<String, MultipartFile> multipartFiles) {
        this.multipartFiles =
                new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles));
    }

    /**
     * 获取this.multipartFiles字段;如果还未初始化,则先初始化(也就是说,本类支持懒初始化该字段)
     */
    protected MultiValueMap<String, MultipartFile> getMultipartFiles() {
        if (this.multipartFiles == null) {
            initializeMultipart();
        }
        return this.multipartFiles;
    }

    /**
     * 初始化this.multipartFiles字段;本方法需要被子类覆盖
     */
    protected void initializeMultipart() {
        throw new IllegalStateException("Multipart request not initialized");
    }

    /**
     * 判断底层的请求是否被解析了;由于本类支持懒解析,因此需要提供该方法,方便外界判断请求是否真的解析了
     */
    public boolean isResolved() {
        return (this.multipartFiles != null);
    }

    /**
     * 获取底层的ServletRequest对象;由于我们能确定它就是HttpServletRequest类型的,因此进行了强转
     */
    @Override
    public HttpServletRequest getRequest() {
        return (HttpServletRequest) super.getRequest();
    }

    /**
     * 获取Http请求方式
     */
    @Override
    public HttpMethod getRequestMethod() {
        return HttpMethod.resolve(getRequest().getMethod());
    }

    /**
     * 获取Http请求头
     */
    @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);
    }
    
    // getFiles()/getFileMap()/getMultiFileMap()方法底层都是在调用getMultipartFiles()方法,因此省略
}

3.3. StandardMultipartHttpServletRequest

/**
 * 本类继承自AbstractMultipartHttpServletRequest
 * 本类主要负责包装HttpServletRequest请求,并将其底层的Part对象封装成MultipartFile对象
 */
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {

    /**
     * 用于存放普通的表单项的参数名(这些参数名对应的值是字符串,而不是文件数据)
     */
    @Nullable
    private Set<String> multipartParameterNames;

    /**
     * 构造方法1:需指定底层的HttpServletRequest对象,默认立刻解析该请求
     */
    public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
        this(request, false);
    }

    /**
     * 构造方法2:需指定底层的HttpServletRequest对象,以及是否懒初始化
     */
    public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
            throws MultipartException {
        super(request);
        
        // 如果不懒初始化,则立刻解析请求
        if (!lazyParsing) {
            parseRequest(request);
        }
    }

    /**
     * 解析请求
     */
    private void parseRequest(HttpServletRequest request) {
        try {
            // 先获取到该请求的所有Part对象
            Collection<Part> parts = request.getParts();
            
            // 初始化this.multipartParameterNames字段,用于存放普通的表单项参数
            this.multipartParameterNames = new LinkedHashSet<>(parts.size());
            
            // 用于存放上传文件的解析结果
            MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
            
            // 对这些Part对象逐个进行解析
            for (Part part : parts) {
                
                // 获取到Content-Disposition头信息
                String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
                ContentDisposition disposition = ContentDisposition.parse(headerValue);
                
                // 获取到文件名称
                String filename = disposition.getFilename();
                
                // 如果文件名称不为空,说明该Part确实是上传文件的表单项
                if (filename != null) {
                    
                    // 这段代码不用管;好像是和JavaMail相关的
                    if (filename.startsWith("=?") && filename.endsWith("?=")) {
                        filename = MimeDelegate.decode(filename);
                    }
                    
                    // 将该Part封装成StandardMultipartFile对象,并将该解析结果存起来
                    files.add(part.getName(), new StandardMultipartFile(part, filename));
                
                // 否则,说明该Part是普通的表单项,此时将参数名称保存到this.multipartParameterNames中
                } else {
                    this.multipartParameterNames.add(part.getName());
                }
            }
            
            // 将文件解析结果保存到父类中
            setMultipartFiles(files);
        
        } catch (Throwable ex) {
            // 根据ex.getMessage()来抛出对应的运行时异常
            handleParseFailure(ex);
        }
    }

    /**
     * 重写父类的initializeMultipart()方法,对请求进行解析
     */
    @Override
    protected void initializeMultipart() {
        parseRequest(getRequest());
    }

    /**
     * 重写ServletRequestWrapper类的getParameterNames()方法
     * 这里会将this.multipartParameterNames也添加到请求参数列表中
     * 这是因为有的Servlet容器并不会将form表单参数添加到getParameterNames()集合中
     * 
     * 此外,本类还重写了ServletRequestWrapper类的getParameterMap()方法,其逻辑和本方法类似,因此省略
     */
    @Override
    public Enumeration<String> getParameterNames() {
        
        // 如果请求还未解析,则先解析
        if (this.multipartParameterNames == null) {
            initializeMultipart();
        }
        
        // 如果form表单没有提交普通参数,则还是走原来的逻辑
        if (this.multipartParameterNames.isEmpty()) {
            return super.getParameterNames();
        }

        // 获取到请求参数和form表单的普通参数,将它们合并到一起,然后转成Enumeration对象
        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);
    }

    /**
     * 获取某个form表单项参数对应的文件的内容类型
     */
    @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);
        }
    }

    /**
     * 获取某个form表单项对应的文件的头部信息
     */
    @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);
        }
    }
}

4. MultipartResolver

4.1. 概述

/**
 * 解析上传文件的策略接口;本接口不依赖于ApplicationContext,可以单独使用;本接口有两个具体实现:
 * 1. CommonsMultipartResolver:底层依靠commons-fileupload.jar工具包来解析上传的文件;已过时
 * 2. StandardServletMultipartResolver:依靠Servlet 3.0的Part API来解析上传的文件;推荐使用
 * 
 * DispatcherServlet默认不使用MultipartResolver组件,因为用户的应用可能会选择自己来解析上传的文件
 * 如果要启用MultipartResolver组件,则必须将其注册到Mvc容器中,且Bean名称为"multipartResolver"
 * DispatcherServlet会自动根据该名称从Mvc容器中获取MultipartResolver组件,并将其应用到每一个请求中
 * 
 * 除了可以在DispatcherServlet中解析文件上传的请求之外,我们还可以使用过滤器来解析
 * Spring提供了MultipartFilter过滤器来解析文件上传的请求,只需把它注册到Servlet容器中即可
 * MultipartFilter会自动从根容器中获取MultipartResolver组件,一般是在不使用SpringMVC框架时使用
 * 
 * 我们还可以在Controller中注册ByteArrayMultipartFileEditor或StringMultipartFileEditor组件来进行数据绑定
 * 这两个组件会自动将上传文件的内容转成byte[]或String形式
 *
 * 注意,在程序代码中,我们基本不需要和MultipartResolver组件打交道
 * 我们需要做的,顶多就是将文件上传的请求对象转成MultipartHttpServletRequest类型
 */
public interface MultipartResolver {

    /**
     * 判断目标请求是否为文件上传的请求
     */
    boolean isMultipart(HttpServletRequest request);

    /**
     * 将原生的Http请求封装成MultipartHttpServletRequest对象,方便获取上传的文件和form表单的普通参数
     */
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

    /**
     * 清除MultipartHttpServletRequest请求中使用的资源,比如删除上传文件时创建的临时文件
     */
    void cleanupMultipart(MultipartHttpServletRequest request);
}

4.2. StandardServletMultipartResolver

/**
 * MultipartResolver接口的标准实现,基于Servlet 3.0中的Part API
 */
public class StandardServletMultipartResolver implements MultipartResolver {

    /**
     * 是否懒解析,默认为false;省略该字段的set方法
     */
    private boolean resolveLazily = false;

    /**
     * 判断目标请求是否为文件上传的请求;这里只是简单地判断ContentType是否以"multipart/"开头
     */
    @Override
    public boolean isMultipart(HttpServletRequest request) {
        return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
    }

    /**
     * 将原生的Http请求封装成MultipartHttpServletRequest对象
     */
    @Override
    public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
        return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
    }

    /**
     * 清除MultipartHttpServletRequest请求中使用的资源,比如删除上传文件时创建的临时文件
     */
    @Override
    public void cleanupMultipart(MultipartHttpServletRequest request) {
        
        // 如果该请求不是AbstractMultipartHttpServletRequest类型的,那么不管它是否有懒解析机制,都要尝试进行清除
        // 否则,如果它确实已经解析过原始请求了,那么此时也需要执行清除操作
        if (!(request instanceof AbstractMultipartHttpServletRequest) ||
                ((AbstractMultipartHttpServletRequest) request).isResolved()) {
            try {
                // 获取到所有的Part对象,并依次调用其delete()方法
                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);
            }
        }
    }
}

5. DispatcherServlet处理文件上传

5.1. 解析文件上传的请求

我们知道,processRequest()方法主要是在doService()方法前后执行了初始化和清除工作(如初始化/恢复LocaleContext等)
doService()方法则主要对includeforwardredirect请求进行了相应的处理,并将必要的组件存到请求域中
接下来就需要来看doDispatch()方法了;它是真正处理请求的方法:

public class DispatcherServlet extends FrameworkServlet {

    /**
     * 底层使用的MultipartResolver组件;该字段会在DispatcherServlet初始化时被赋值(但初始化后仍有可能为null)
     */
    @Nullable
    private MultipartResolver multipartResolver;

    /**
     * 真正处理请求,这里先省略掉无关的代码
     */
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        
        // processedRequest代表真正要被处理的请求,初始化为原始的request对象
        // 比如,我们有可能需要对原始的request进行包装,而包装后的对象才是真正要被处理的对象
        HttpServletRequest processedRequest = request;
        
        // multipartRequestParsed代表当前请求是否为文件上传的请求,初始化为false
        boolean multipartRequestParsed = false;

        try {
            try {
                // 判断request是否为文件上传的请求,如果是,则对其进行包装,然后将包装后的对象赋值给processedRequest
                processedRequest = checkMultipart(request);
                
                // 如果processedRequest和request不相同,说明processedRequest是包装对象,也就是说,当前请求是文件上传的请求
                multipartRequestParsed = (processedRequest != request);

                // 通过processedRequest而不是request来查找能处理该请求的处理器
                // 后面的所有HttpServletRequest传参,传的都是processedRequest对象
                mappedHandler = getHandler(processedRequest);
                
                // 省略后续的代码
            } catch (Exception ex) {
                dispatchException = ex;
            } catch (Throwable err) {
                dispatchException = new NestedServletException("Handler dispatch failed", err);
            }
            processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
        } catch (Exception ex) {
            triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
        } catch (Throwable err) {
            triggerAfterCompletion(processedRequest, response, mappedHandler,
                    new NestedServletException("Handler processing failed", err));
        } finally {
            if (asyncManager.isConcurrentHandlingStarted()) {
                if (mappedHandler != null) {
                    mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
                }
            } else {
                // 如果当前请求是文件上传的请求,则执行清除操作
                if (multipartRequestParsed) {
                    cleanupMultipart(processedRequest);
                }
            }
        }
    }

    /**
     * 判断目标请求是否为文件上传请求;如果是,则将其包装为MultipartHttpServletRequest类型
     */
    protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
        
        // 如果底层的MultipartResolver组件不为null,并且该组件判断当前请求确实是文件上传的请求
        if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
            
            // 当前的request可能经过了层层包装,因此我们需要判断这些包装中,是否有MultipartHttpServletRequest装饰器
            // 如果已经被MultipartHttpServletRequest装饰器包装了(如配置了MultipartFilter),则忽略
            if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
                if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
                    logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
                }
            
            // 否则,如果该请求中有MultipartException异常,说明当前是在进行异常处理?此时也忽略
            } 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;
                    }
                }
            }
        }
        
        // 执行到这里,说明MultipartResolver组件为null,或者不需要包装
        return request;
    }

    /**
     * 执行上传文件的清除操作
     */
    protected void cleanupMultipart(HttpServletRequest request) {
        if (this.multipartResolver != null) {
            
            // 从request的层层包装中获取到MultipartHttpServletRequest装饰器
            // 然后调用MultipartResolver组件来处理该MultipartHttpServletRequest
            MultipartHttpServletRequest multipartRequest =
                    WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
            if (multipartRequest != null) {
                this.multipartResolver.cleanupMultipart(multipartRequest);
            }
        }
    }
}

5.2. 控制器方法接收上传的文件

我们可以在控制器方法中添加PartMultipartFile类型的参数来直接接收用户上传的文件信息,如:

@RestController("/upload")
public class FileUploadController {

    /**
     * 这里的@RequestParam@RequestPart注解的作用相似
     * 这两个注解都可以省略,此时形参名称将作为表单项的参数名称来进行查找
     * 此外,控制器方法还支持Part或MultipartFile的数组/集合形式的参数
     */
    @PostMapping("/demo")
    public String testPost(
            @RequestParam("file") Part file1, @RequestPart("file") Part file2,
            @RequestParam("file") MultipartFile file3, @RequestPart("file") MultipartFile file4
    ) {
        System.out.println(file1 == file2); // true
        System.out.println(file3 == file4); // true
        return "success";
    }
}

上面这两个注解分别由RequestParamMethodArgumentResolverRequestPartMethodArgumentResolver组件来解析
我们以RequestParamMethodArgumentResolver为例来了解PartMultipartFile类型的参数的解析过程:

public class RequestParamMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver
        implements UriComponentsContributor {

    /**
     * 判断是否支持解析目标方法参数
     */
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        
        // 如果该参数上有@RequestParam注解
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            
            // 如果该参数是Map类型
            if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                
                // 如果@RequestParam注解指定了具体的参数名称,则支持
                // 否则,说明用户想通过Map来接收所有的请求参数,此时应该由RequestParamMapMethodArgumentResolver来处理
                RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
                return (requestParam != null && StringUtils.hasText(requestParam.name()));
            
            // 否则,支持处理该参数
            } else {
                return true;
            }
        
        // 如果没有@RequestParam注解
        } else {
            
            // 如果有@RequestPart注解,则不支持
            if (parameter.hasParameterAnnotation(RequestPart.class)) {
                return false;
            }
            
            // 如果参数类型是Part或MultipartFile类型,或者是它们的数组/集合形式,则支持
            // 因此,这些类型的参数是不需要有@RequestParam注解的
            parameter = parameter.nestedIfOptional();
            if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
                return true;
            
            // 剩下的不管,可以视为返回false
            } else if (this.useDefaultResolution) {
                return BeanUtils.isSimpleProperty(parameter.getNestedParameterType());
            } else {
                return false;
            }
        }
    }

    /**
     * 解析方法参数
     */
    @Override
    @Nullable
    protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
        
        // 获取到HttpServletRequest对象;如果有,则尝试进行解析
        HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
        if (servletRequest != null) {
            
            // 判断该请求是否为上传文件的请求,并且当前参数是Part或MultipartFile等类型,如果是,则提取出相应的数据
            Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
            
            // 如果确实解析到了相应的数据,则直接返回
            if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
                return mpArg;
            }
        }

        // 否则,再尝试从MultipartRequest对象中提取出对应的MultipartFile信息
        Object arg = null;
        MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
        if (multipartRequest != null) {
            List<MultipartFile> files = multipartRequest.getFiles(name);
            if (!files.isEmpty()) {
                arg = (files.size() == 1 ? files.get(0) : files);
            }
        }
        
        // 如果arg为null,说明当前参数不是文件类型的,此时直接读取请求参数即可
        if (arg == null) {
            String[] paramValues = request.getParameterValues(name);
            if (paramValues != null) {
                arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
            }
        }
        return arg;
    }
}
public final class MultipartResolutionDelegate {

    /**
     * 解析控制器方法中的文件类型的参数
     */
    @Nullable
    public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
            throws Exception {

        // 判断当前请求是否为上传文件的请求
        // 如果能获取到MultipartHttpServletRequest装饰器,或者ContentType以"multipart/"开头,则是上传文件的请求
        MultipartHttpServletRequest multipartRequest =
                WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
        boolean isMultipart = (multipartRequest != null || isMultipartContent(request));

        // 如果参数是MultipartFile类型
        if (MultipartFile.class == parameter.getNestedParameterType()) {
            
            // 如果当前不是上传文件的请求,则返回null
            if (!isMultipart) {
                return null;
            }
            
            // 必要的话,将该请求封装成StandardMultipartHttpServletRequest
            if (multipartRequest == null) {
                multipartRequest = new StandardMultipartHttpServletRequest(request);
            }
            
            // 通过StandardMultipartHttpServletRequest对象来获取相应的MultipartFile对象
            return multipartRequest.getFile(name);
        
        // 如果参数是Collection<MultipartFile>或List<MultipartFile>等类型
        } else if (isMultipartFileCollection(parameter)) {
            if (!isMultipart) {
                return null;
            }
            if (multipartRequest == null) {
                multipartRequest = new StandardMultipartHttpServletRequest(request);
            }
            List<MultipartFile> files = multipartRequest.getFiles(name);
            return (!files.isEmpty() ? files : null);
        
        // 如果参数是MultipartFile[]类型
        } else if (isMultipartFileArray(parameter)) {
            if (!isMultipart) {
                return null;
            }
            if (multipartRequest == null) {
                multipartRequest = new StandardMultipartHttpServletRequest(request);
            }
            List<MultipartFile> files = multipartRequest.getFiles(name);
            return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null);
        
        // 如果参数是Part类型
        } else if (Part.class == parameter.getNestedParameterType()) {
            if (!isMultipart) {
                return null;
            }
            
            // 直接通过getPart(name)方法来进行解析
            return request.getPart(name);

        // 如果参数是Collection<Part>或List<Part>等类型
        } else if (isPartCollection(parameter)) {
            if (!isMultipart) {
                return null;
            }
            List<Part> parts = resolvePartList(request, name);
            return (!parts.isEmpty() ? parts : null);
        
        // 如果参数是Part[]类型
        } else if (isPartArray(parameter)) {
            if (!isMultipart) {
                return null;
            }
            List<Part> parts = resolvePartList(request, name);
            return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null);
        
        // 否则,说明是普通类型,这种情况不归当前类管,因此返回UNRESOLVABLE
        } else {
            return UNRESOLVABLE;
        }
    }

    /**
     * 遍历所有Part对象,并将名称与name匹配的Part对象保存下来并返回
     */
    private static List<Part> resolvePartList(HttpServletRequest request, String name) throws Exception {
        Collection<Part> parts = request.getParts();
        List<Part> result = new ArrayList<>(parts.size());
        for (Part part : parts) {
            if (part.getName().equals(name)) {
                result.add(part);
            }
        }
        return result;
    }
}