最近在项目中,使用springmvc 进行上传文件时,出现了一个问题:
org.springframework.web.multipart.MultipartException: The current request is not a multipart request
....
以上堆栈信息省略。
乍看一下,没啥值得讨论的地方,就是说当前这个请求不是一个multipart request,也就是说不是上传文件的请求。但是,这结果还是令我稍感意外,为什么呢?因为,我本意是将文件这个参数作为非必要参数,类似下面这样:
@RequestMapping(value = "/upload", method = RequestMethod.POST) public ResultView upload(@RequestParam(value = "file", required = false) MultipartFile file)
spring抛出上面的异常,就违背了我的本意,我明明设置了 “required = false”, 为什么还是不行? 于是,带着疑问去看了一下spring的源码,下面就跟大家分享一下spring mvc对于文件上传的处理。
--------------------------------------------------我是华丽的分割线-------------------------------------------------------
在spring mvc通过DispatcherServlet处理请求时,会调用到 doDispatch这个方法,当然这也是spring mvc处理请求最核心的方法:
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);
上面就是给出的有关上传文件的代码片段,看以看到,当spring处理请求的时候,首先第一步就去检查当前请求是否为上传文件的请求,那么,它是怎么检查的呢,接着往下看:
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 (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) instanceof MultipartException) { logger.debug("Multipart resolution failed for current request before - " + "skipping re-resolution for undisturbed error rendering"); } else { return this.multipartResolver.resolveMultipart(request); } } // If not returned before: return original request. return request; }
通过以上方法,我们可以看到如下逻辑:
(1)当 MultipartResolver 不为null的时候, 就通过它去检查当前请求是否为文件上传请求(通过CommonsMultipartResolver的isMultipart方法)。
(2)如果当前请求不是MultipartHttpServletReques并且不包含MultipartException异常,那么,就通过CommonsMultipartResolver去处理当前请求(通过调用resolveMultipart方法将当前请求包装为MultipartHttpServletRequest),返回包装后的请求。
(3)返回当前请求(未经处理的请求)。
接下来我们重点看看,spring是如何判断是否为文件上传的请求的:
CommonsMultipartResolver:
@Override public boolean isMultipart(HttpServletRequest request) { return (request != null && ServletFileUpload.isMultipartContent(request)); }
这儿直接使用了Apache 的commons-fileupload中的ServletFileUpload, 那我们就来看看它究竟何许人也:
ServletFileUpload:
public static final boolean isMultipartContent( HttpServletRequest request) { if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) { return false; } return FileUploadBase.isMultipartContent(new ServletRequestContext(request)); }
以上代码说明:
(1)当前请求必须是post方法。
(2)如果是post方法,就通过 FileUploadBase 去进一步检测。
FileUploadBase:
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; }
以上方法说明:
只有当当前请求的contentType是 "multipart/" 的时候,才会将此请求当做文件上传的请求。
总结:
综合来看,spring其实是通过Apache的 commons-fileupload来检测请求是否为文件上传的请求。而commons-fileupload又是通过如下两个条件来判断:
1. 请求方法必须是 post.
2. 请求的contentType 必须设置为以 "multipart/" 开头。
这下你该明白为什么我们在上传文件的时候必须要做的那些设置了吧。
好啦,回到文章开始的问题:
org.springframework.web.multipart.MultipartException: The current request is not a multipart request
这个问题是怎么导致的呢?
其实springmvc 在处理方法入参的时候,发现了你的一个参数为 MultipartFile 类型或者是其数组或者包含他的容器类型,那么springmvc 就会通过上面类似的方法去检验(通过contentType)。代码如下:
private void assertIsMultipartRequest(HttpServletRequest request) { String contentType = request.getContentType(); if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) { throw new MultipartException("The current request is not a multipart request"); } }
那么这个问题该如何解决呢?
(1)ContentType 必须设置为 multipart/ 开头的(通常是multipart/form-data)。我之所以会遇到这个问题,其实是因为在APP请求的时候明明使用的multipart/form-data,但是却始终通不过,尝试用浏览器OK。
(2)在保证(1)的情况,如果还是这个错误,那么通过上面的分析,其实也很好解决,怎么解决?
spring在处理入参的时候, 不是遇到MultipartFile相关就会先去校验么,OK,利用这个,那么咱们可以改写入参(直接接收原生的http request),然后自己手动去校验啊对吧,这不就绕过了。当绕过这一步之后,springmvc会通过之前分析的代码,对收到的请求进行校验转换,最终也会得到MultipartHttpServletRequest。修改如下:
@RequestMapping(value = "/upload", method = RequestMethod.POST) public ResultView upload(HttpServletRequest request) { if (request instanceof MultipartHttpServletRequest) { // process } }
OK, 这样就通过了。
好啦,本篇就先写到这儿,下篇将向大家谈谈springmvc上传文件的效率问题。