08-MultipartResolver

2022-07-18

08-MultipartResolver

springmvc之MultipartResolver

一. 概述

上传文件的组件,主要分为以下两种方式:

StandardServletMultipartResolverCommonsMultipartResolver,前者采用Servlet3.0标准的上传方式,后者则使用了Apache的commons-fileupload

二. 源码分析

2.1 MultipartResolver

boolean isMultipart(HttpServletRequest request);

MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;

void cleanupMultipart(MultipartHttpServletRequest request);

这个接口中定义了三个方法

  1. isMultipart:request中是否包含文件内容
  2. resolveMultipart:负责解析request中的文件和参数,并将结果包装为MultipartHttpServletRequest
  3. cleanupMultipart:用于清理资源

在DispatchServlet中,如下的代码使用到了该组件

processedRequest = checkMultipart(request);

处理一个请求首先就开始检查是不是携带文件信息。

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
		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 {
					return this.multipartResolver.resolveMultipart(request);
				}
				//省略异常处理
			}
		}
		// If not returned before: return original request.
		return request;
	}

这里需要注意的是,在携带文件信息的前提下,如果已经有了MultipartHttpServletRequest,那么直接返回。

否则调用resolveMultipart处理请求

2.2 StandardServletMultipartResolver

使用Servlet3.0标准的文件上传方式。

@Override
public boolean isMultipart(HttpServletRequest request) {
  return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}

判断是否是上传请求的标示为:content-type以multipart/"开头

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

处理请求的方式为:直接返回一个StandardMultipartHttpServletRequest对象

public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
  throws MultipartException {

  super(request);
  if (!lazyParsing) {
    parseRequest(request);
  }
}
private void parseRequest(HttpServletRequest request) {
		try {
			Collection<Part> parts = request.getParts();
			this.multipartParameterNames = new LinkedHashSet<>(parts.size());
			MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
			for (Part part : parts) {
				String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
				ContentDisposition disposition = ContentDisposition.parse(headerValue);
				String filename = disposition.getFilename();
				if (filename != null) {
					if (filename.startsWith("=?") && filename.endsWith("?=")) {
						filename = MimeDelegate.decode(filename);
					}
					files.add(part.getName(), new StandardMultipartFile(part, filename));
				}
				else {
					this.multipartParameterNames.add(part.getName());
				}
			}
			setMultipartFiles(files);
		}
		catch (Throwable ex) {
			handleParseFailure(ex);
		}
	}

下面通过一个实际发送文件的请求,来看看这段代码中具体的处理

Controller

@RequestMapping("/test9")
@ResponseBody
public String test9(MultipartFile[] files) {
  for (MultipartFile file : files) {
    System.out.println(file.getOriginalFilename());
  }
  return "success";
}

postman

image-20210622105939518

  1. request.getParts()

image-20210621152327791

​ 可以看到,Part中封装了文件的信息。名称,content-type等等

  1. 遍历parts,依次获取part中的信息,并封装到MultipartHttpServletRequest中

    • 解析Content-Disposition

      form-data; name="files[0]"; filename="2.jpg"

      image-20210621152824480

    • 封装到MultiValueMap中

      files.add(part.getName(), new StandardMultipartFile(part, filename));

      这是一个key为文件的filedName,value为MultipartFile的map。与普通map的区别是value是一个list,可以存储多个值

经过上面的处理,StandardMultipartHttpServletRequest对象就封装好了,返回给DispatchServlet使用。最后在doDispatch的final中调用了清除资源的方法

if (multipartRequestParsed) {
  cleanupMultipart(processedRequest);
}

2.3 springboot自动配置

在springboot中默认使用的就是StandardServletMultipartResolver,下面看看它是怎么去自动注入该组件的

@Configuration(proxyBeanMethods = false)
@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;
  }

}

这个类很简单,主要注入了两个类,引入了一个资源文件

MultipartProperties是一个配置类,主要配置上传文件的一些参数

private String location;

//文件的最大尺寸  默认为1M
private DataSize maxFileSize = DataSize.ofMegabytes(1);

//上传请求的最大尺寸 默认为10M
private DataSize maxRequestSize = DataSize.ofMegabytes(10);

//当文件达到多少时进行磁盘写入
private DataSize fileSizeThreshold = DataSize.ofBytes(0);

我们可以在application.properties中修改这些配置

spring:
    servlet:
      multipart:
        enabled: true #是否启用http上传处理
        max-request-size: 100MB #最大请求文件的大小
        max-file-size: 20MB #设置单个文件最大长度
        file-size-threshold: 20MB #当文件达到多少时进行磁盘写入

三. 使用技巧

3.1 在全局异常中处理上传错误

springmvc对于上传错误的封装如下:

Collection<Part> parts = request.getParts();

protected void handleParseFailure(Throwable ex) {
	String msg = ex.getMessage();
	if (msg != null && msg.contains("size") && msg.contains("exceed")) {
		throw new MaxUploadSizeExceededException(-1, ex);
	}
	throw new MultipartException("Failed to parse multipart servlet request", ex);
}

其中的异常主要是request.getParts();产生的,对于其中的细节不做分析。结论如下:

由于新建MaxUploadSizeExceededException时传入的最大大小限制都为-1,因此这里获取不到具体的大小限制,需要从异常的cause中获取

package com.mahaonan.springbootpractice.component;

import com.mahaonan.springbootpractice.bean.ResponseResult;
import com.mahaonan.springbootpractice.constant.MessageCode;
import com.mahaonan.springbootpractice.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.FileUploadBase;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import tools.STUtils;

import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: M˚Haonan
 * @Description: 自定义全局异常处理类
 */
@ControllerAdvice
@Slf4j
public class MyExceptionHandler {

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public ResponseResult handleException(Exception ex, HttpServletRequest request) throws Exception {
        ResponseResult result = new ResponseResult();
        if (ex instanceof ServiceException) {
            ServiceException e = (ServiceException) ex;
            result.setCode(e.getCode());
            result.setMsg(e.getMessage());
        }else if (ex instanceof MaxUploadSizeExceededException) {
            setFileUploadException(result, ex);
        } else {
            result.setCode(MessageCode.SYSTEM_INTERNAL_ERROR.getCode());
            result.setMsg(MessageCode.SYSTEM_INTERNAL_ERROR.getMessage());
        }
        log.error("system error:", ex);
        return result;
    }

    private void setFileUploadException(ResponseResult result, Throwable e) {
        long maxUploadSize = -1;
        MessageCode messageCode = null;
        do {
            if(e instanceof MaxUploadSizeExceededException) {
                //这里maxUploadSize总为-1
                maxUploadSize = ((MaxUploadSizeExceededException)e).getMaxUploadSize();
                messageCode = MessageCode.UPLOAD_SINGLE_MAX_LIMIT_SIZE;
            } else if(e instanceof FileUploadBase.FileSizeLimitExceededException) {
                maxUploadSize = ((FileUploadBase.FileSizeLimitExceededException)e).getPermittedSize();
                messageCode = MessageCode.UPLOAD_SINGLE_MAX_LIMIT_SIZE;
            }else if (e instanceof FileUploadBase.SizeLimitExceededException) {
                //request size最大限制在这个异常类中体现
                maxUploadSize = ((FileUploadBase.SizeLimitExceededException)e).getPermittedSize();
                messageCode = MessageCode.UPLOAD_TOTAL_MAX_LIMIT_SIZE;
            }
            e = e.getCause();
        } while (maxUploadSize == -1 && e != null);
        if (messageCode != null) {
            setResult(result, messageCode, String.valueOf(maxUploadSize / 1024));
        }else {
            setResult(result, MessageCode.SYSTEM_INTERNAL_ERROR);
        }
    }

    private void setResult(ResponseResult result, MessageCode messageCode, String...params) {
        result.setCode(messageCode.getCode());
        if (params.length > 0) {
            STUtils.STBuilder stBuilder = STUtils.simple(messageCode.getMessage());
            try {
                for (int i = 0; i < params.length; i++) {
                    stBuilder.add("param" + i, params[i]);
                }
                result.setMsg(stBuilder.build().render());
            }catch (Exception e) {
                e.printStackTrace();
                result.setMsg(messageCode.getMessage());
            }
        }else {
            result.setMsg(messageCode.getMessage());
        }
    }
}

public enum MessageCode {


    NOT_LOGIN(12001, "未登录"),
    LOGIN_EXPIRE(12002, "未登录"),

    SERVICE_INTERNAL_ERROR(20001, "业务处理失败"),

    UPLOAD_SINGLE_MAX_LIMIT_SIZE(20002, "上传文件不能超过{param0}kb"),

    UPLOAD_TOTAL_MAX_LIMIT_SIZE(20003, "上传文件总大小不能超过{param0}kb"),

    SYSTEM_INTERNAL_ERROR(90001, "服务异常"),

}

完整代码如下,可以处理异常

3.2 多个文件上传

多个文件上传Controller的接收如下:

@RequestMapping("/test9")
@ResponseBody
public String test9(@RequestParam("files") MultipartFile[] files) {
  for (MultipartFile file : files) {
    System.out.println(file.getOriginalFilename());
  }
  return "success";
}

postman发送如下:

image-20210622105729928

需要注意的就是:请求参数的名字,都为files。因为上传组件默认就支持多个上传,在参数解析的时候,会根据我们在controller中定义的files作为name去寻找。

3.3 Content-Disposition

该属性是header中的属性,既可以用在request中,也可以用在response中

3.3.1 用在request中

在上面的分析中,我们看到,springMV会从该属性中寻找文件名,如果找到,就作为文件的上传名字。因此用在request中,就是作为上传文件的名称保存在StandardMultipartFile中,提供给我们使用

request中格式如下:

Content-Disposition:form-data; name="files"; filename="2.jpg"

表明了上传的name是files,fileName是2.jpg

3.3.2 用在response中

当我们下载文件的时候,有时候是直接在浏览器打开,有时候则是弹出一个下载框,这就是该属性在其中发挥了作用

  1. 直接显示内容

    @RequestMapping("/test17")
    public void test17(HttpServletResponse response) throws Exception{
      response.setContentType("application/xml");
      response.setCharacterEncoding("UTF-8");
      File file = new File("/Users/mahaonan/mhn/javaProject/springboot-practice/pom.xml");
      byte[] bytes = FileUtil.readBytes(file);
      ServletOutputStream outputStream = response.getOutputStream();
      response.setContentLength(bytes.length);
      outputStream.write(bytes);
      outputStream.flush();
      outputStream.close();
      System.out.println("test17执行了");
    }
    

    这里必须明确指定Content-Type为何种类型,才能够直接在浏览器打开

  2. 显示下载框

    response.setHeader("Content-Disposition", "attachment; filename=\"pom.xml\"");
    

    只需要设置Content-Disposition即可

四. 总结

​ 对于上传组件,没什么特别的地方,只需要知道springboot默认采用的是servlet3.0标准的上传方式。其次知道怎么去接受文件,怎么去配置上传参数即可。


标题:08-MultipartResolver
作者:mahaonan
地址:https://mahaonan.fun/articles/2022/07/18/1658147018441.html