問題:使用Spring MVC上傳大文件,發現從頁面提交,到進入后台controller,時間很長。懷疑是文件上傳完成后,才進入。由於在HTTP首部自定義了“Token”字段用於權限校驗,Token的有效時間很短,因此上傳大文件時,就會驗證Token失敗。
示例代碼:
前端:
<form action="upload" enctype="multipart/form-data" method="post"> <table> <tr> <td>文件描述:</td> <td><input type="text" name="description"></td> </tr> <tr> <td>請選擇文件:</td> <td><input type="file" name="file"></td> </tr> <tr> <td><input type="submit" value="上傳"></td> </tr> </table> </form>
controller:
@RequestMapping(value="/upload",method=RequestMethod.POST) public String upload(HttpServletRequest request, @RequestParam("description") String description, @RequestParam("file") MultipartFile file) throws Exception { System.out.println("enter controller."); // 文件上傳完才打印 }
springmvc-config.xml配置:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <property name="maxUploadSize"> <value>524288000</value> </property> <property name="defaultEncoding"> <value>UTF-8</value> </property> </bean>
Spring MVC上傳文件使用了Commons FileUpload類庫,即CommonsMultipartResolver使用commons Fileupload來處理 multipart請求,將Commons FileUpload的對象轉換成了Spring MVC的對象。
那如果直接使用Commons FileUpload來進行文件上傳下載呢?
示例代碼:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("enter servlet"); // 頁面提交后,立馬打印
// 省略獲取上傳文件的邏輯
// ...
}
發現從頁面提交,很快就進入了servlet。
既然Spring也是使用了Commons FileUpload類庫,但為什么差別這么大呢?Spring在轉換過程中做了什么其他操作呢?
查看源碼,或者直接看([Java] SpringMVC工作原理之四:MultipartResolver)
發現有個resolveLazily參數是判斷是否要延遲解析文件(通過XML可以設置)。當 resolveLazily為false(默認)時,會立即調用 parseRequest() 方法對請求數據進行解析,然后將解析結果封裝到 DefaultMultipartHttpServletRequest中;而當resolveLazily為 true時,會在DefaultMultipartHttpServletRequest的initializeMultipart()方法調用parseRequest()方法對請求數據進行解析,而initializeMultipart()方法又是被getMultipartFiles()方法調用,即當需要獲取文件信息時才會去解析請求數據,這種方式用了懶加載的思想。
再次修改代碼:
1、在springmvc-config.xml增加一個配置resolveLazily,設置true(如何配置見文末);
2、將controller方法中將@RequestParam("file") MultipartFile file注釋
再次測試,發現還是不對。。增加打印日志后發現:
@RequestMapping(value="/upload",method=RequestMethod.POST) public String upload(HttpServletRequest request, @RequestParam("description") String description /*@RequestParam("file") MultipartFile file*/) throws Exception { System.out.println("enter controller."); // 還是文件上傳完后,才打印 System.out.println(request.getHeader("Accept")); System.out.println(request.getParam("description"));
// 省略獲取上傳文件的邏輯 }
再注釋@RequestParam("description") String description
@RequestMapping(value="/upload",method=RequestMethod.POST) public String upload(HttpServletRequest request /*@RequestParam("description") String description, */ /*@RequestParam("file") MultipartFile file*/) throws Exception { System.out.println("enter controller."); // 頁面提交后,立刻打印 System.out.println(request.getHeader("Accept")); // 幾乎也是立刻打印 System.out.println(request.getParam("description")); // 文件上傳完,才打印
// 省略獲取上傳文件的邏輯
}
這次,第1,2條日志很快打印,第3條日志仍是文件上傳完后才打印。簡單分析下,應該是后台一直在接收數據,HTTP首部很快就能獲取到並解析出來(首部和正文之間有一個空行),但請求正文部分必須在請求數據完全接收后才能解析,因此線程一直阻塞直到數據接收完成才打印第3條日志。因此解決方案是,把controller函數中的@RequestParam("description") String description也注釋掉,這樣頁面提交后,會立即進入controller中。
總結下最終的解決方案:
1、springmvc-config.xml增加一項配置:
<property name="resolveLazily "> <value>true</value> </property>
2、在controller的方法參數中,不能附加請求參數,應在函數中自己解析參數。