UEditor上傳圖片與spring mvc上傳圖片衝突問題。
HTML 頁面中的表單最初所採用 application/x-www-form-urlencode 編碼方式,並不滿足檔案上傳的需要,所以,RFC 1867 在此基礎上增加了新的 multipart/form-data 編碼方式以支援基於表單的檔案上傳。通常情況下,按照如下形式宣告表單以及表單中的元素:
- <formaction="..."method="post"enctype="multipart/form-data">
- <inputtype="file"name="tile2upload"/>
- <inputtype="submit"value="Upload"/>
- </form>
既然 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圖片上傳衝突的問題了。