最近在項目中,使用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, 這樣就通過了。