1. 程式人生 > 其它 >記SpringMVC+Ajax上傳檔案失敗問題解決

記SpringMVC+Ajax上傳檔案失敗問題解決

SpringMVC+Ajax上傳檔案異常排查,涉及HTTP報文,Jquery Ajax API,JS FormData API, Spring MVC解析HTTP報文的Heades,Fiddler抓包,POSTMAN發起POST請求。

因為也是第一次寫上傳檔案程式碼,所在工程也沒有現場的程式碼可以copy,就開始了面向搜尋的程式設計。

網上的教程的寫法一般都是,前端form表單,然後把表單取出後放在FormData物件中,ajax的寫法如下

1 <script type="text/javascript">
 2     $(function () {
 3         $("input[type='button']").click(function () {
 4             var formData = new FormData($("#upForm")[0]);
 5             $.ajax({
 6                 type: "post",
 7                 url: "${pageContext.request.contextPath}/upfile/upload",
 8                 data: formData,
 9                 cache: false,
10                 processData: false,
11                 contentType: false,
12                 success: function (data) {
13                     alert(data);
14                 },
15                 error: function (response) {
16                     console.log(response);
17                     alert("上傳失敗");
18                 }
19             });
20         });
21     });
22 </script>
23 <body>
24     <form id="upForm" method="post" enctype="multipart/form-data">
25         使用者名稱:<input type="text" name="userName" id="userName" /><br/>
26         密碼:<input type="password" name="pwd" id="pwd" /><br/>
27         <input type="file" name="image"><br/>
28         <input type="button" value="提交" />
29     </form>
30 </body>
31 </html>

SpringMVC的配置檔案,也就是把apache的兩個Jar檔案整合進來

1 <!-- 定義檔案上傳解析器 -->
 2 <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
 3    <property name="defaultEncoding">
 4       <value>UTF-8</value>
 5    </property>
 6    <property name="maxUploadSize">
 7       <value>32505856</value><!-- 上傳檔案大小限制為31M,31*1024*1024 -->
 8    </property>
 9    <property name="maxInMemorySize">
10       <value>4096</value>
11    </property>
12 </bean>

後臺的Controller

1 @Controller
 2 @RequestMapping("/upfile")
 3 public class UpFileController {
 4     @RequestMapping("/upload")
 5     @ResponseBody
 6     public String getMsg(UserTest user,@RequestParam("image") CommonsMultipartFile file){
 7         System.out.println(user.getUserName());
 8         System.out.println(file.getOriginalFilename());
 9         return "接收成功";
10     }
11 }

通過搜尋引擎換關鍵詞看了好幾篇博文,都類似這種寫法,但我執行之後都是丟擲500,或者400異常,500異常後臺丟擲org.springframework.web.multipart.MultipartException: The current request is not a multipart request,400的話因為沒有開啟DEBUG級別的日誌,但知道是引數不匹配的問題,我就把引數限定為只需要file這個檔案,之後就一直是丟擲500錯誤。

開始排錯之旅,一開始自然是繼續搜,但發現網上的都是千篇一律,寫法都一樣,那沒辦法了,只能找差異,然後各種嘗試,比如改formData封裝的寫法,改UpFileController的註解、引數,改CommonsMultipartResolver的引數。像一隻無頭蒼蠅。

沒辦法了,請教第一個高階程式設計師同事,他一來就讓我用PostMan調一下,Body填form-data,填好key和選擇檔案,成功打到Controller的方法內了,那就確認了後臺程式碼的邏輯是正確的,那我就通過fiddler比對成功和失敗兩次請求的報文,方法、路徑、協議版本號自然是一樣的;headers裡面的差異是content-type,

失敗的也就是通過Ajax呼叫的,為false,multipart/form-data;boundary--xxx;成功的為form-data/data;boundary--xxx;Body裡面,因為是multipart/form-data,訊息體裡面就是form表單裡面的引數,檔案流本身的也有個content-type;成功的為如下圖所示為text/x-java-source,失敗的檔案流型別為application/octet-stream。對比出差異了,那現在就想著怎麼讓Ajax發出的報文和正確的一致了。

一開始又走錯了,一直搜Ajax上傳檔案,給到寫法也是一樣的,然後又是走了老路,看了FormDate這個API,並沒有設定content-type的地方;又嘗試了把Ajax的設定中,contentType: false這個屬性的註釋,改成multipart/form-data,但報文依舊無法和成功的一致。這時候又開始折騰後臺的引數了,希望後臺可以正常解析這個報文(注,我也沒好好看看丟擲異常的這行程式碼,沒有看到Spring判斷content-type是用的startWith),自然還是失敗了。

繼續請教同事,前端的同事讓我不要老是看別人怎麼寫的,要看官方的API,但是也看了下我的寫法,感覺也沒有不對,就走了;繼續請求大佬,也沒看出有什麼問題,因為這個時候剛下班,同事就都回去了。我又接原地打轉,又嘗試了一個半小時,還是沒有解決,就是吃點東西散散步了。

回來坐了回,有個朋友過來看我還在加班,自然他也剛加班完,我就抓住他,把問題複述了一邊,發現可以通過postman,設定檔案的型別,就把mutilpart/form-data中檔案這個值的content-type也設定為了application/octet-stream,但postman的請求還是成功了;繼續比對報文,發現是headers裡面的content-type還是為false,multipart/form-data;boundary--xxx;這個時候我就去看了眼spring丟擲異常的地方,發現spring限制了報文必須為multipart開頭。

1     private void assertIsMultipartRequest(HttpServletRequest request) {
2         String contentType = request.getContentType();
3         if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) {
4             throw new MultipartException("The current request is not a multipart request");
5         }
6     }

這個時候也知道了是Ajax的問題,想起前端同事說的,我開始去jQuery看Ajax的文件,如下

contentType (default: 'application/x-www-form-urlencoded; charset=UTF-8')
Type: Boolean or String
When sending data to the server, use this content type. Default is "application/x-www-form-urlencoded; charset=UTF-8", which is fine for most cases. 
If you explicitly pass in a content-type to $.ajax(), then it is always sent to the server (even if no data is sent).
As of jQuery 1.6 you can pass false to tell jQuery to not set any content type header.
Note: The W3C XMLHttpRequest specification dictates that the charset is always UTF-8; specifying another charset will not force the browser to change the encoding. 
Note: For cross-domain requests, setting the content type to anything other than application/x-www-form-urlencoded, multipart/form-data, or text/plain will trigger the browser to send a preflight OPTIONS request to the server.

很明顯,需要1.6的以上,我看了下工程用的版本是1.4的,複製個其他專案高版本的Jquery過來,報文headers不是false,multipart/form-data;boundary--xxx而是正確的multipart/form-data;boundary--xxx了。

總結如下:

  1. 嘗試不同的呼叫方式,如果一開始就使用了Postman,就能確定後臺程式碼的正確了

  2. 確定環境一致,要確定copy別人程式碼時,確保和別人,使用的瀏覽器,框架的版本是否大致一致

  3. 檢視框架丟擲的錯誤棧程式碼;雖然網上也說了是multipart/form-data的問題,但一開始以為只要content-type包含了multipart/form-data即可

  4. 檢視API的相關文件;雖然通過Ajax關鍵詞搜尋,網上都是現成的寫法,但作為一個API,方法和引數還是有個迭代的過程,網上都說要用false設定Ajax的contenttype屬性,卻沒有說明這個引數的要求的版本

  5. 系統學習理論只是,如果對HTTP協議理論瞭解更深入點,就知道,是一個非法的符號(在幾次嘗試中Spring也丟擲了這個異常)

1     /** org.springframework.http.MediaType.checkToken(String)
 2      * Checks the given token string for illegal characters, as defined in RFC 2616, section 2.2.
 3      * @throws IllegalArgumentException in case of illegal characters
 4      * @see <a href="http://tools.ietf.org/html/rfc2616#section-2.2">HTTP 1.1, section 2.2</a>
 5      */
 6     private void checkToken(String s) {
 7         for (int i=0; i < s.length(); i++ ) {
 8             char ch = s.charAt(i);
 9             if (!TOKEN.get(ch)) {
10                 throw new IllegalArgumentException("Invalid token character '" + ch + "' in token \"" + s + "\"");
11             }
12         }
13     }

Content-Type