likes
comments
collection
share

SpringMVC流程分析(三):MultipartResolver组件——SpringMVC中处理上传请求的关键

作者站长头像
站长
· 阅读数 15

本系列文章皆在分析SpringMVC的核心组件和工作原理,让你从springmvc浩如烟海的代码中跳出来,以一种全局的视角来重新审视SpringMVC的工作原理SpringMVC.

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜

前言

在上一章SpringMVC流程分析(二):揭开DispatcherServlet的神秘面纱 中,我们分析清楚了 DispatcherServlet对于Http请求的处理主要委托于内部的doDispatch方法进行完成,而doDispatch调用链如下所示:

SpringMVC流程分析(三):MultipartResolver组件——SpringMVC中处理上传请求的关键

本章我们聚焦其中的chekMultipart方法,并以此为基础分析SpringMVC中的用于处理上传请求的MultipartResolver组件。

SpringMVC流程分析(三):MultipartResolver组件——SpringMVC中处理上传请求的关键

checkMulipart的内部处理逻辑

在开始分析MultipartResolver组件之前,我们先进入到checkMultipart方法内部,从而了解其内部的处理过程。checkMultipart代码如下:

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
  // 判断是否为一个multipart 请求
   if (this.multipartResolver != null 
               && this.multipartResolver.isMultipart(request)) {
     // .....省略其他无关代码
     // 将请求封装成 MultipartHttpServletRequest类型,并将请求中参数解析为文件
     return this.multipartResolver.resolveMultipart(request);
        
   }
   
   return request;
}

  • 首先,该方法会调用isMultipart检查传入的 HttpServletRequest 请求对象中是否为文件上传的请求。
  • 随后,该方法会调用resolveMultipart方法,来将请求中的上传信息解析为文件对象的形式进行保存,从而方便后续处理。

上述checkMultipart方法内部所调用的isMultipartresolveMultipart正是组件MultipartResolver内部所定义的方法。接下来,我们将深入分析MultipartResolver的相关内容。

概览MultipartResolver组件

MultipartResolverSpringMVC中的一个重要的组件。具体来看,MultipartResolver 的作用主要是对文件上传的请求进行解析,并将文件数据提取出来,从而方便后续直接获取上传数据,而无需再手动的对上传数据进行解析和处理。其中,MultipartResolver接口定义如下所示:

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

上述方法的具体作用如下:

  1. boolean isMultipart(HttpServletRequest request): 这个方法用来判断当前请求是否为 multipart 请求,即判断请求的 Content-Type 是否为 multipart/form-data
  2. MultipartHttpServletRequest resolveMultipart(HttpServletRequest request): 这个方法用于将 multipart 请求解析为 MultipartHttpServletRequest 对象。MultipartHttpServletRequestSpring 提供的一个扩展 HttpServletRequest 接口的类,它可以让你方便地获取文件上传的数据。
  3. void cleanupMultipart(MultipartHttpServletRequest request): 在文件上传处理完成后,需要调用这个方法来清理临时的文件和资源。

MultipartResolver的体系结构

SpringMVC流程分析(三):MultipartResolver组件——SpringMVC中处理上传请求的关键

上图展示了两部分内容:

  • 上半部分展示了MultipartRequest 接口及其实现类,MultipartRequest中实现的方式不同,则resolveMultipart方法的解析出的MultipartRequest会有所差异。但不管怎么变化,其本质都是MultipartRequest类型。

  • 下半部分展示了MultipartResolver 接口以及其实现类, SpringMVC中对于MultipartResolver 的组件的默认实现有两种,分别为StadardMultipartHttpServletRequstCommonMultipartResolver

(注:resolveMultipart的处理逻辑相当于替换了上传请求信息,从而将其包装成为一个MultipartHttperServletRequest

不同MultipartResolver实现类间的区别

通过上图我们注意到,MultipartResolver 默认有两个具体的实现分别为CommonsMultipartResolverStandardServletMultipartResolver。接下来,我们深入到的其内部源码,来深入分析两者间的区别。其中StandardServletMultipartResolver的内容如下:

public class StandardServletMultipartResolver 
                            implements MultipartResolver {
    
    
    @Override
    public boolean isMultipart(HttpServletRequest request) {
        // 请求的 Content-type 必须 multipart/ 开头
        return StringUtils.startsWithIgnoreCase(request.getContentType(), 
                                                            "multipart/");
    }
    
    @Override
    public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
        // 将请求包装为一个StandardMultipartHttpServletRequest类型
        return new StandardMultipartHttpServletRequest(request, 
                                                       this.resolveLazily);
    }
     

StandardServletMultipartResolver中,isMultipart方法在判断请求时通过判断Conten-type中是否包含multipart/字段来判断请求是否为一个上传请求; 而resolveMultipart对于请求的处理,其主要是对HttpServletRequest进行分封装并返回一个StandardMultipartHttpServletRequest的对象,从而方便后续直接从Request中获取上传数据流中的文件信息。

事实上,CommonsMultipartResolver 中的逻辑与其大致相似,所以我们便不再重复论述。下表展示了CommonsMultipartResolverStandardServletMultipartResolver间的具体差异。

组件StandardServletMultipartResolverCommonsMultipartResolver
实现接口MultipartResovlerMultipartResovler
依赖组件Commons FileUpload
请求封装StandardMultipartHttpServletRequestDefaultMultipartHttpServletRequest

为了方便后续内容理解,在此我们将对前文叙述内容进行一次总结梳理,以梳理清楚本文的叙述脉络。 首先,我们的主要目的在于探究Http 请求在 DispatcherServlet 中的处理过程。 对此重点关注了其内部的 doDispatch 方法,由于checkMultipartdoDispatch方法中最先执行的方法。所以我们以此为突破口,分析了checkMultipart的内部逻辑。研究表明,该方法会通过 MultipartResolver 组件提供的isMultipartresolveMultipart方法来判断是否为上传请求,并进行请求封装。随后,我们将关注的重点落点到 MultipartResolver 组件,分析了组件的功能、类层次结构以及其实现类 CommonsMultipartResolverStandardServletMultipartResolver 的差异。结果表明,CommonsMultipartResolverStandardServletMultipartResolver间的差异主要在于方法resolveMultipart对于请求的封装会返回不同的MultipartRequest

所以,接下来我们会将关注的重点放在MultipartRequest的分析上。

概览MultipartRequest

我们注意到,CommonsMultipartResolverStandardServletMultipartResolver之间之间最大的差异来源于resolveMultipart方法中对于HttpServletRequst的封装有所区别。因此,接下来我们将深入分析StandardMultipartHttpServletRequestDefaultMultipartHttpServletRequest 的差异。

MultipartRequest接口

Spring MVC 中,MultipartRequest 是一个接口,它继承了 javax.servlet.http.HttpServletRequest,用于处理文件上传的请求。当客户端通过表单提交包含文件上传的请求(multipart/form-data)时,MultipartResolver 会将原始的 HttpServletRequest 请求对象进行封装,并生成一个实现了 MultipartRequest 接口的对象。而StandardMultipartHttpServletRequestDefaultMultipartHttpServletRequestSpringMVC中常用到的两个类 。

StandardMultipartHttpServletRequest的秘密

Spring MVC 中,StandardMultipartHttpServletRequestMultipartRequest 接口的一个实现类在 Servlet 3.0+ 环境下,当客户端通过表单提交包含文件上传的请求时,StandardServletMultipartResolver 将原始的 HttpServletRequest 请求对象进行解析,并生成一个 StandardMultipartHttpServletRequest 实例,该实例是对文件上传请求的封装。相关代码如下:

public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {
   
    // ... 省略其他无关代码
    public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) throws MultipartException {
        super(request);
        // 如果不需要延迟解析
        if (!lazyParsing) {
            // 解析请求
            parseRequest(request);
        }
    }
    // ... 省略其他无关代码
}

不难发现,在StandardMultipartHttpServletRequest的构造其中,会直接调用parseRequest方法,来完成对于请求的处理。相关代码如下所示:

StandardMultipartHttpServletRequest#parseRequest()

private void parseRequest(HttpServletRequest request) {
    try {
        // <1> 从 HttpServletRequest 中获取 Part 们
        // `Part` 接口提供了标准的方法来获取上传文件的信息和内容
        Collection<Part> parts = request.getParts();
       
        // <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 数组

    • Part 对象中获得请求头中的 Content-Disposition 信息,MIME 协议的扩展
    • Content-Disposition 信息进行解析,生成 ContentDisposition 对象
    • ContentDisposition 对象中获得文件名
    • 情况一,文件名非空,说明是文件参数,则创建 StandardMultipartFile 对象
    • 情况二,文件名为空,说明是普通参数,则保存参数名称
  3. 将上面生成的 StandardMultipartFile 文件对象们,设置到父类的 multipartFiles 属性中,方便后续调用

注:Part 接口提供了标准的方法来获取上传文件的信息和内容,同时上述解析完成文件的对象类型为StandardMultipartFile

DefaultMultipartHttpServletRequest的秘密

DefaultMultipartHttpServletRequestSpringMVC中另一个用于处理文件上传的请求的类。其同样实现了它实现了MultipartRequest 接口,区别于StandardMultipartHttpServletRequest的主要地方在于其使用时基于 Apache Commons FileUpload 库的组件信息。

StandardMultipartHttpServletRequest一样,我们重点关注其内部方法parseRequest()的实现:

DefaultMultipartHttpServletRequest#parseRequest()

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);
    }
  // ....省略异常捕获相关代码
}

可以看到其处理逻辑与StandardMultipartHttpServletRequest#parseRequest()中的逻辑大致相似,所以便不再重复叙述。

总的来看, DefaultMultipartHttpServletRequestStandardMultipartHttpServletRequest 都是 SpringMVC 中用于处理多部分请求的实现类,用于处理文件上传和访问上传的文件。它们的具体实现方式有所不同,但提供了类似的方法来获取上传的文件和其他表单字段的值。 DefaultMultipartHttpServletRequestSpringMVC 自己的实现,而 StandardMultipartHttpServletRequest 则是基于Servlet 3.0+ 规范的实现。

总结

本文以DispatcherServlet中的doDispatch方法为切入点,重点分析了其中checkMultipart的方法,讨论了SpringMVC 在处理请求的过程中使用到的 MultipartResolver 组件。在此基础上,研究了MultipartResolver组件对于请求的转换处理。具体来看,其可将 HttpServletRequest 请求对象封装成 MultipartHttpServletRequest 对象,从而方便从请求中获取获取参数信息和并将上传数据信息转为 MultipartFile 对象,以方便后续操作。

除此之外,如果我们自定义一个Http请求解析也是可以,其关键就在于我们需要修改isMultipart的判定逻辑,以及resolveMultipart的处理逻辑。同时,还应该保证经过resolveMultipart方法处理后,可以返回一个ServletRequest的对象。

注意事项:

  • Spring MVC 中,multipartResolver 默认为 null,需要自己配置,此时可配置MultipartResolverCommonsMultipartResolver 实现类,也可以配置为 StandardServletMultipartResolver 实现类

  • 在 Spring Boot 中,multipartResolver 默认为 StandardServletMultipartResolver 实现类,如果要更换则需要将MultipartResolver 的自动装配进行关闭,可通过 @SpringBootApplication(exclude = MultipartAutoConfiguration.class)或配置文件形式进行实现。