Spring 文件上传使用的总结及MultipartFile的解析

文件上传

前后台交互

使用MultipartFile参数接收:

单文件上传

  1. 前端<form>标签

    1
    2
    3
    4
    <form action="/UploadOneServlet.do" method="post" name="f_upload" enctype="multipart/form-data">
    <input type="file" name="multipartFile" />
    <input type="submit" value="上传" />
    </form>

    <form>标签中添加enctype="multipart/form-data",即可实现multipart类型数据上传。

  2. 后台接口

    1
    2
    3
    4
    5
    @RequestMapping(value = "/UploadOneServlet.do", method = RequestMethod.POST)
    @ResponseBody
    public String upload(@RequestParam("multipartFile") MultipartFile multipartFile) {
    //...文件处理代码
    }

    后端中的对应方法中使用MultipartFile类型参数进行接收。必须添加@RequestParam("multipartFile")参数,否则报错。@RequestParamvalue与前端imput标签中的name对应。

一个input标签上传多个文件

  1. 前端<form>标签
    1
    2
    3
    4
    <form action="/UploadOneServlet.do" method="post" name="f_upload" enctype="multipart/form-data">
    <input type="file" name="multipartFiles" multiple/>
    <input type="submit" value="上传" />
    </form>
    <input>标签中添加multiple参数即可在一个框中选择多个文件上传
  2. 后台接口
    1
    2
    3
    4
    5
    @RequestMapping(value = "/UploadOneServlet.do", method = RequestMethod.POST)
    @ResponseBody
    public String upload(@RequestParam("multipartFile") List<MultipartFile> multipartFile) {
    //...文件处理代码
    }
    后台使用List,即可前台上传的多个文件。如果使用单个MultipartFile类型进行接收则只能接收一个文件。

多个name相同的单文件input标签

  1. 前端<form>标签
    1
    2
    3
    4
    5
    6
    <form action="/UploadOneServlet.do" method="post" name="f_upload" enctype="multipart/form-data">
    <input type="file" name="multipartFiles" />
    <input type="file" name="multipartFiles" />
    <input type="file" name="multipartFiles" />
    <input type="submit" value="上传" />
    </form>
    前台的多个<input>标签的name相同
  2. 后台接口
    1
    2
    3
    4
    @RequestMapping(value = "/UploadOneServlet.do", method = RequestMethod.POST)
    @ResponseBody
    public String upload(@RequestParam("multipartFiles") List<MultipartFile> multipartFiles) {
    }
    后台接收方法与一个input标签上传多个文件时相同,使用List<MultipartFile>进行接收。@RequestParamvalue与前端imput标签中的name对应。

混合使用

  1. 前端<form>标签

    1
    2
    3
    4
    5
    <form action="/UploadOneServlet.do" method="post" name="f_upload" enctype="multipart/form-data">
    <input type="file" name="multipartFiles" />
    <input type="file" name="multipartFile" multiple/>
    <input type="submit" value="上传" />
    </form>

    前台使用name不同的<input>标签上传文件

  2. 后台接口

    1
    2
    3
    4
    @RequestMapping(value = "/UploadOneServlet.do", method = RequestMethod.POST)
    @ResponseBody
    public String upload(@RequestParam("multipartFile") List<MultipartFile> multipartFiles, @RequestParam("multipartFiles") MultipartFile multipartFile) {
    }

    后台使用一个List<MultiPartFile>类型和MultiPartFile类型进行接收。@RequestParamvalue与前端imput标签中的name一一对应。

使用HttpServletRequest接收

  1. 前端<input>标签
    1
    2
    3
    4
    5
    <form action="/UploadOneServlet.do" method="post" name="f_upload" enctype="multipart/form-data">
    <input type="file" name="multipartFiles" />
    <input type="file" name="multipartFile" multiple/>
    <input type="submit" value="上传" />
    </form>
  2. 后台接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @RequestMapping(value = "/UploadOneServlet.do", method = RequestMethod.POST)
    @ResponseBody
    public String upload(HttpServletRequest request){
    List<MultipartFile> files = new ArrayList<>();
    MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
    Iterator<String> a = multipartHttpServletRequest.getFileNames();//返回的数量与前端input数量相同, 返回的字符串即为前端input标签的name
    while (a.hasNext()) {
    String name = a.next();
    List<MultipartFile> multipartFiles = multipartHttpServletRequest.getFiles(name);//获取单个input标签上传的文件,可能为多个
    files.addAll(multipartFiles);
    }
    upload(files);
    return "success";
    }

文件上传

之前的几种情况已经接收到了前台上传的List<MultipartFile>文件集合,遍历集合实现文件上传

1
2
3
4
5
6
7
8
9
private void upload(List<MultipartFile> multipartFiles) throws Exception{
for (MultipartFile multipartFile : multipartFiles) {
String fileName = multipartFile.getOriginalFilename();
String filePath = ContextLoader.getCurrentWebApplicationContext().getServletContext().getRealPath("/") + "fileUpload";
String fileTotalName = filePath + File.separator + fileName;
File f = new File(fileTotalName);
multipartFile.transferTo(f);
}
}

MultiPartFile类解析

Spring MVC中要使用MultipartFile进行文件上传,需要先注入MultipartResolver类型的Bean:

1
2
3
4
5
6
7
8
<!-- 支持上传文件 -->
<bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<!-- 设置上传文件的最大尺寸为50MB -->
<property name="maxUploadSize">
<value>52428800</value>
</property>
</bean>

之前解析DispatcherServlet类时得知所有最终会调用这个类中的doDispatch方法对request进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null || mappedHandler.getHandler() == null) {
noHandlerFound(processedRequest, response);
return;
}
//...剩余代码
}
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()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

可以看到processedRequest = checkMultipart(request)对reuqest进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +
"this typically results from an additional MultipartFilter in web.xml");
}
else if (hasMultipartException(request) ) {
logger.debug("Multipart resolution failed for current request before - " +
"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);
}
else {
throw ex;
}
}
}
}
return request;
}

通过以上方法可以看到以下逻辑:

  1. 判断用户是否注入了MultipartResolver类型的Bean,如果注入了则使用它去检测当前请求是否为文件上传请求
  2. 如果为文件上传请求,就通过MultipartResolver去处理当前请求。
  3. 返回处理后的请求或为处理的请求。

文件上传请求判断

接下来看看Spring是如何判断当前请求是文件上传请求的, 这里主要看CommonsMultipartResolver

1
2
3
public boolean isMultipart(HttpServletRequest request) {
return (request != null && ServletFileUpload.isMultipartContent(request));
}

这里直接使用了Apache中的ServletFileUpload:

1
2
3
4
5
6
7
public static final boolean isMultipartContent(
HttpServletRequest request) {
if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {
return false;
}
return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}

这里可以看到request请求必须是POST类型,如果是,则用FileUploadBase进行进一步判断:

1
2
3
4
5
6
7
8
9
10
public static final boolean isMultipartContent(RequestContext ctx) {
String contentType = ctx.getContentType();
if (contentType == null) {
return false;
}
if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {// MULTIPART = "multipart/"
return true;
}
return false;
}

以上方法说明:
只有当当前请求的contentType是以”multipart/“开头的时候,才会将此请求当做文件上传的请求。

综上,Spring判断当前请求是否是文件上传请求主要有两个条件:

  1. 当前请求为POST请求
  2. 当前请求的contextType必须以”multipart/“开头
    这正符合了前端<form>标签中的设置。

文件上传请求解析

如果是文件上传请求,则会调用multipartResolver.resolveMultipart(request)对请求进行解析:

1
2
3
4
5
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
MultipartParsingResult parsingResult = parseRequest(request);
return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
}

可以看到任然是调用parseRequest方法对request进行解析,并将结果存入MultipartParsingResult

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
String encoding = determineEncoding(request);
FileUpload fileUpload = prepareFileUpload(encoding);
try {
// 获取到上传文件的信息 一个FileItem对应一个文件,FileItem的fieldName与input框的name对应
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
//将其存入MultipartParsingResult类中
return parseFileItems(fileItems, encoding);
}
catch (FileUploadBase.SizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
}
catch (FileUploadBase.FileSizeLimitExceededException ex) {
throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
}
catch (FileUploadException ex) {
throw new MultipartException("Failed to parse multipart servlet request", ex);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String, MultipartFile>();
Map<String, String[]> multipartParameters = new HashMap<String, String[]>();
Map<String, String> multipartParameterContentTypes = new HashMap<String, String>();

// Extract multipart files and multipart parameters.
for (FileItem fileItem : fileItems) {
if (fileItem.isFormField()) {
//...省略代码
}
else {
// multipart file field
CommonsMultipartFile file = createMultipartFile(fileItem);
// file.getName()得到fileItem.getfieldName map键位input框的name, 值为具体文件类
multipartFiles.add(file.getName(), file);
if (logger.isDebugEnabled()) {
logger.debug("Found multipart file [" + file.getName() + "] of size " + file.getSize() +
" bytes with original filename [" + file.getOriginalFilename() + "], stored " +
file.getStorageDescription());
}
}
}
return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
}

初始化了MultipartParsingResult对象之后就能根据该对象去初始化MultipartHttpServletRequest对象,供接口中调用。