Spring MVC提供MultipartResolver接口来实现对文件上传的支持,Spring MVC本身并未有此接口的实现,需借助第三方jar实现。
MultipartResolver的两种实现:
- Commons FileUpload
- Servlet 3.0
Commons FileUpload上传文件的使用
要使用Commons FileUpload首先需要引入如下依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
然后向spring web容器中注入一个名为multipartResolver的MultipartResolver对象(注意名字必须为multipartResolver,后面看源码就知道为什么?)
@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
在上传文件的请求方法中加入MultipartFile参数,spring mvc会自动将文件封装成此参数:
package com.morris.spring.mvc.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
/**
* 演示文件的上传
*/
@Slf4j
@Controller
@RequestMapping("file")
public class FileController {
@GetMapping("index")
public String index() {
return "file";
}
/**
* 不需要MultipartResolver
* @param file
* @return
*/
@PostMapping("uploadFile")
public String uploadFile(@RequestBody String file) {
log.info("file content is {}", file);
return "success";
}
/**
* 需要MultipartResolver,否则无法解析
* @param file
* @param desc
* @return
* @throws IOException
*/
@PostMapping("uploadFile2")
public String uploadFile(MultipartFile file, String desc) throws IOException {
file.transferTo(new File("d:\\" + file.getOriginalFilename()));
log.info("file content is {}", file);
log.info("file description is {}", desc);
return "success";
}
}
注意:@RequestParam("file")注解可以不加,但是参数名必须与页面表单的字段名保持一致,如MultipartFile file。
在html页面上传时, enctype必须为multipart/form-data。
<form action="/file/uploadFile" method="post" enctype="multipart/form-data">
Choose File1: <input type="file" name="file">
<br/>
Description: <input type="text" name="desc">
<br/>
<input type="submit" value="Submit">
</form>
<br/><br/>
<form action="/file/uploadFile2" method="post" enctype="multipart/form-data">
Choose File2: <input type="file" name="file">
<br/>
Description: <input type="text" name="desc">
<br/>
<input type="submit" value="Submit">
</form>
Commons FileUpload的源码分析
DispatchServlet注入MultipartResolver
Spring MVC容器在启动后会发送一个ContentRefresh消息,DispatchServlet监听到消息后会调用如下方法:
org.springframework.web.servlet.DispatcherServlet#initMultipartResolver
public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
private void initMultipartResolver(ApplicationContext context) {
...
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
...
}
Spring MVC会在容器中寻找一个名为multipartResolver的MultipartResolver,所以上面注入的MultipartResolver的名字只能是multipartResolver。
将请求包装为MultipartHttpServletRequest
当一个请求过来时,Spring MVC都会检查此请求中是否有文件,如果有就调用MultipartResolver(前提是已注入MultipartResolver,否则会直接返回)的resolveMultipart方法,这里会将请求包装成MultipartHttpServletRequest:
org.springframework.web.servlet.DispatcherServlet#checkMultipart
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);
}
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;
}
怎么判断一个请求是否有文件?来自multipartResolver.isMultipart方法:
org.springframework.web.multipart.commons.CommonsMultipartResolver#isMultipart
public boolean isMultipart(HttpServletRequest request) {
return ServletFileUpload.isMultipartContent(request);
}
org.apache.commons.fileupload.servlet.ServletFileUpload#isMultipartContent
public static final boolean isMultipartContent(
HttpServletRequest request) {
if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {
return false;
}
return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}
org.apache.commons.fileupload.FileUploadBase#isMultipartContent(org.apache.commons.fileupload.RequestContext)
public static final boolean isMultipartContent(RequestContext ctx) {
String contentType = ctx.getContentType();
if (contentType == null) {
return false;
}
if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {
return true;
}
return false;
}
当请求是一个POST请求并且ContentType以multipart/开头就会认为是一个文件请求。
Servlet3.0上传文件的使用
Servlet3.0中使用文件上传,首先需要开启Servlet容器的配置(不加MultipartConfigElement,文件无法上传),在无web.xml中可以如下配置:
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
MultipartConfigElement multipartConfigElement = new MultipartConfigElement("/tmp", -1, -1, 100*1024);
registration.setMultipartConfig(multipartConfigElement);
}
在controller中与Commons FileUpload中的使用一致。
MultipartConfigElement可以限制上传文件的临时目录(这个临时目录在必须先创建好,servlet容器不会自动创建,不存在则会抛出异常),上传文件的大小,文件大小超过多少会生成临时文件(这个临时文件生成在临时目录中,请求完成后会自动删除),单位为byte。
注意:Servlet3.0的文件上传完全由Servlet容器完成,与spring mvc无关,MultipartConfigElement是servlet api。
Servlet3.0上传文件的源码分析
StandardServletMultipartResolver的自动注入
当一个文件上传时,Servlet容器会根据MultipartConfigElement来自动上传文件(根据配置是否上传至临时目录),如果没有配置MultipartConfigElement,文件将不会上传。
Spring MVC会根据目标方法中是否包含MultipartFile参数来确定是否使用StandardMultipartHttpServletRequest,代码如下:
org.springframework.web.multipart.support.MultipartResolutionDelegate#resolveMultipartArgument
public static MultipartRequest resolveMultipartRequest(NativeWebRequest webRequest) {
MultipartRequest multipartRequest = webRequest.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
return multipartRequest;
}
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null && isMultipartContent(servletRequest)) {
// 包装为StandardMultipartHttpServletRequest
return new StandardMultipartHttpServletRequest(servletRequest);
}
return null;
}
StandardMultipartHttpServletRequest的构造方法中会对servlet容器上传的文件进行解析:
public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
this(request, false);
}
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
parseRequest(request);
}
}
其实,在调用Spring MVC的DispatchServlet之前,文件已经上传至临时目录,SpringMVC只是将文件的路径封装一下,供目标方法使用:
private void parseRequest(HttpServletRequest request) {
try {
// 从request直接拿上传好的文件进行再次封装
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);
}
}
本文介绍SpringMVC中文件上传的实现方式,包括通过CommonsFileUpload和Servlet3.0进行文件上传的方法,并分析了其核心组件MultipartResolver的工作原理。
463

被折叠的 条评论
为什么被折叠?



