1. 程式人生 > >SpirngMVC檔案上傳與下載org.springframework.web.multipart.MultipartException: The current request is not a m

SpirngMVC檔案上傳與下載org.springframework.web.multipart.MultipartException: The current request is not a m

最近在專案中,使用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上傳檔案的效率問題。