08-MultipartResolver
08-MultipartResolver
springmvc之MultipartResolver
一. 概述
上传文件的组件,主要分为以下两种方式:
StandardServletMultipartResolver
和CommonsMultipartResolver
,前者采用Servlet3.0标准的上传方式,后者则使用了Apache的commons-fileupload
二. 源码分析
2.1 MultipartResolver
boolean isMultipart(HttpServletRequest request);
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
void cleanupMultipart(MultipartHttpServletRequest request);
这个接口中定义了三个方法
isMultipart
:request中是否包含文件内容resolveMultipart
:负责解析request中的文件和参数,并将结果包装为MultipartHttpServletRequestcleanupMultipart
:用于清理资源
在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
- request.getParts()
可以看到,Part中封装了文件的信息。名称,content-type等等
-
遍历parts,依次获取part中的信息,并封装到MultipartHttpServletRequest中
-
解析Content-Disposition
form-data; name="files[0]"; filename="2.jpg"
-
封装到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发送如下:
需要注意的就是:请求参数的名字,都为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中
当我们下载文件的时候,有时候是直接在浏览器打开,有时候则是弹出一个下载框,这就是该属性在其中发挥了作用
-
直接显示内容
@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为何种类型,才能够直接在浏览器打开
-
显示下载框
response.setHeader("Content-Disposition", "attachment; filename=\"pom.xml\"");
只需要设置
Content-Disposition
即可
四. 总结
对于上传组件,没什么特别的地方,只需要知道springboot默认采用的是servlet3.0标准的上传方式。其次知道怎么去接受文件,怎么去配置上传参数即可。