1. 程式人生 > >UEditor上傳圖片與spring mvc上傳圖片衝突問題。

UEditor上傳圖片與spring mvc上傳圖片衝突問題。

HTML 頁面中的表單最初所採用 application/x-www-form-urlencode 編碼方式,並不滿足檔案上傳的需要,所以,RFC 1867 在此基礎上增加了新的 multipart/form-data 編碼方式以支援基於表單的檔案上傳。通常情況下,按照如下形式宣告表單以及表單中的元素:

  1. <formaction="..."method="post"enctype="multipart/form-data">
  2.   <inputtype="file"name="tile2upload"/>
  3.   <inputtype="submit"value="Upload"/>
  4. </form>
客戶端瀏覽器將按照 RFC 1867 所規定的格式,對提交表單內容進行編碼,伺服器端只需要根據 RFC 1867 規定的格式對請求中的資訊進行解碼,就可以獲得客戶端表單提交的資料,包括上傳的檔案。

        既然 RFC 1867 所規定的規則是一定的,所以,我們沒有必要每次都根據這一規則分析每一次請求中的資訊。既然是通用的邏輯,當然也就有通用的類庫,比如早期的 jsp smart upload 和 Oreilly 的 COS 類庫,以及現在使用最多的 Commons FileUpload 類庫。實際開發中,我們只需要使用這些專門針對表單的檔案上傳處理類庫即可。

        在實際基於表單的檔案上傳功能的時候,Spring MVC 框架底層實際上也是使用了以上幾種類庫。只不過,通過 org.springframework.web.multipart.MultipartResolver 介面的抽象,Spring MVC 將具體選用哪一種類庫的權利留給了我們。


        MultipartResolver 位於 HandlerMapping 之前,請求一來就交由它來處理。當 Web 請求到達 DispatcherServlet 並等待處理的時候,DispatcherServlet 首先會檢查能否從自的 WebApplicationContext 中找到一個名稱為 multipartResolver(由 DispatcherServlet 的常量 MULTIPART_RESOLVER_BEAN_NAME 所決定)的 MultipartResolver 例項。如果能夠獲得一個 MultipartResolver 的例項,DispatcherServlet 將呼叫 MultipartResolver 的 isMultipart(request) 方法檢查當前 Web 請求是否為 multipart型別。如果是,DispatcherServlet 將呼叫 MultipartResolver 的 resolveMultipart(request) 方法,對原始 request 進行裝飾,並返回一個 MultipartHttpServletRequest 供後繼處理流程使用(最初的 HttpServletRequest 被偷樑換柱成了 MultipartHttpServletRequest),否則,直接返回最初的 HttpServletRequest。來看看 UML 類圖:

MultipartRequest 畢竟是介面,介面就是介面,總得有人實現。AbstractMultipartHttpServletRequest 這個抽象類持有 MultiValueMap<String, MultipartFile> multipartFiles 這樣一個例項變數,有了這個 map,把 MultipartRequest 接口裡的方法逐一實現就不是難事了。現在的問題是,multipartFiles 從哪來的?不可能像孫悟空似的從石縫裡蹦出來吧。。。。。

        再回到 MultipartResolver。MultipartResolver 的 isMultipart(request) 方法好實現,當判斷出當前的 request 是 multipart 型別的請求,它將呼叫 MultipartResolve 的 resolveMultipart(request)。這裡的 request 就是原始的 HttpServletRequest 物件,奇蹟就出現在這裡。以 CommonsMultipartResolver 為例,當呼叫 resolveMultipart(request) 時,看看它是如何建立 MultipartRequest 的:


    public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {  
      Assert.notNull(request, "Request must not be null");  
      if (this.resolveLazily) {  
        return new DefaultMultipartHttpServletRequest(request) {  
          @Override  
          protected void initializeMultipart() {  
            MultipartParsingResult parsingResult = parseRequest(request);  
            setMultipartFiles(parsingResult.getMultipartFiles());  
            setMultipartParameters(parsingResult.getMultipartParameters());  
          }  
        };  
      }  
      else {  
        MultipartParsingResult parsingResult = parseRequest(request);  
        return new DefaultMultipartHttpServletRequest(  
            request, parsingResult.getMultipartFiles(), parsingResult.getMultipartParameters());  
      }  
    }  

暫且不管 resolveLazily 為何意。假設 resolveLazily 為 false,我們看 else 的片段。由於是 CommonsMultipartResolver,它的 parseRequest 方法將從原始的 HttpServletRequest 中解析出檔案,得到基於 Commons FileUpload API 的 FileItem 物件。Spring 在這裡封裝了一下,對於 MultipartResolver 而言,它看到的就是 MultipartFile。注意最後的 return,它將構建一個 DefaultMultipartHttpServletRequest,也就是 MultipartRequest。它將 MultipartFile 和 MultipartParameter 作為建構函式的引數傳入,在這個建構函式裡,有 setMultipartFiles 這句話。這個方法正是 AbstractMultipartHttpServletRequest 裡的方法,這樣,AbstractMultipartHttpServletRequest 的例項變數 multipartFiles 就有正規來源了吧,即解決了上面我們提到的疑問。然去實現 MultipartRequest 接口裡的方法就是輕而易舉的事了。

        再來看看 resolveLazily。request 被裝飾了一下,後續處理上傳的檔案,通過 multipartRequest.getFile(name) 就可以拿到檔案。MultipartRequest 接口裡定義的方法全在 AbstractMultipartHttpServletRequest 類裡給實現了,而它之所以能實現,因為它持有了 multipartFiles。雖說是例項變數,但拿到該變數,還是要通過方法得到的。我們來看看 AbstractMultipartHttpServletRequest 裡是如何得到 multipartFiles 的:


    /**
     * Obtain the MultipartFile Map for retrieval,
     * lazily initializing it if necessary.
     * @see #initializeMultipart()
     */  
    protected MultiValueMap<String, MultipartFile> getMultipartFiles() {  
      if (this.multipartFiles == null) {  
        initializeMultipart();  
      }  
      return this.multipartFiles;  
    }  
      
    /**
     * Lazily initialize the multipart request, if possible.
     * Only called if not already eagerly initialized.
     */  
    protected void initializeMultipart() {  
      throw new IllegalStateException("Multipart request not initialized");  
    }  


我們來分析一下以上程式碼。multipartFiles 會為 null 嗎?為什麼要做這樣的判斷?不是之前通過 DefaultMultipartHttpServletRequest 的建構函式傳入了嗎?這裡就是 resolveLazily 的作用了。如果非延遲解析,則的確會通過 DefaultMultipartHttpServletRequest 的建構函式傳入 multipartFiles。如果為延遲解析,則不會傳入 multipartFiles,那麼它當然就有可能為 null 了。multipartFiles 為 null 就會呼叫 initializeMultipart 來初始化(誰讓它延遲呢)。resolveLazily 為 true 時,構造的 DefaultMultipartHttpServletRequest 的物件覆寫了 AbstractMultipartHttpServletRequest 的 initializeMultipart 方法,它從原始請求中解析檔案。思考一個問題:resolveLazily 為 true,直接構造 DefaultMultipartHttpServletRequest 而不覆寫 initializeMultipart 會有什麼後果?

        我認為,resolveLazily 為 false 時,請求一旦被 MultipartResolver 接手,它就會解析請求中的檔案,而不必等待後續 controoler 主動從 MultipartRequest 中 getFile。 resolveLazily 為 true 時,只有等後續的 controller 主動呼叫 MultipartRequest.getFile 才會從原始請求中解析檔案。Spring 這樣處理,可能是考慮效率問題吧。也許是 multipart 型別的請求,但後續又不操作檔案,就沒有在請求一來就做檔案解析操作吧。

注:上面我們提到過DispatcherServlet 將呼叫 MultipartResolver 的 isMultipart(request) 方法檢查當前 Web 請求是否為 multipart型別,因此我們可以重寫CommonsMultipartResolver類中的isMultipart方法,如果請求過來的地址是UEditor圖片上傳的路徑,返回false,不對圖片的資料流進行處理,程式碼如下。

public class CommonsMultipartResolverPhhc extends CommonsMultipartResolver{
    @Override
    public boolean isMultipart(HttpServletRequest request) {
        String url = request.getRequestURI();
        if (url!=null && url.contains("ueditorDispatch.do")) {
            return false;
        } else {
            return super.isMultipart(request);
        }
    }
}

把重寫的這類類替換spring mvc本身提供的類。


這樣就可以解決UEditor和spring mvc圖片上傳衝突的問題了。