這兩天研究了一下 SpringMVC 中文件上傳與下載,也遇到了一些坑,這裏做個總結。
1、文件上傳下載的原理
Web 中文件上傳下載是和 HTTP 協議分不開的,想要更加深入的理解文件上傳和下載,必須要對 HTTP 協議有充分認識。
1.1 文件上傳
在 TCP/IP 中,最早出現的文件上傳機制是 FTP,這是將文件由客戶端發送到服務器的標準機制。而在 Web 開發中,使用應用層協議 HTTP,通過在請求頭中設置傳輸的內容類型 Content-Type 爲 multipart/form-data; boundary=流分隔符值
來上傳文件,這個流分隔符用來區分一個文件上傳的開始和結束,下面的是我在火狐瀏覽器中截取的多個文件上傳時的消息頭和參數。
文件上傳消息頭.jpg
文件上傳參數.jpg
對應在 HTML 中就是爲 form 元素設置 Method = "post" enctype="`multipart/form-data" 屬性,爲 input 元素設置 type = "file" 以及多個文件上傳時設置 "multiple" 屬性,代碼示例如下。
<form action="" method="post" enctype="multipart/form-data" onsubmit="return check()"> <input type="file" name="file" id="file" multiple="multiple"><br> <input type="submit" value="上傳"> </form>
對錶單中的 enctype 屬性做個詳細的說明:
- application/x-www=form-urlencoded:默認方式,只處理表單域中的 value 屬性值,採用這種編碼方式的表單會將表單域中的值處理成 URL 編碼方式。
- multipart/form-data:這種編碼方式會以二進制流的方式來處理表單數據,這種編碼方式會把文件域指定文件的內容也封裝到請求參數中,不會對字符編碼。
- text/plain:除了把空格轉換爲 "+" 號外,其他字符都不做編碼處理,這種方式適用直接通過表單發送郵件。
1.2 文件下載
通過在響應消息頭中設置 Content-Disposition 和 Content-Type 使得瀏覽器無法使用某種方式或者激活某個程序來處理 MIME 類型的文件,來讓瀏覽器提示是否保存文件,也就是文件的下載。Content-Disposition 的值爲 attachment;filename=文件名
,Content-Type 的值爲 application/octet-stream
或者 application/x-msdownload
。文件中的中文注意編碼問題,不同瀏覽器之間是有差異的。
文件下載.jpg
2、SpringMVC中的文件上傳與下載
本文涉及的所以代碼,都可以在我的 GitHub 上找到,傳送門。
2.1 文件上傳
文件在上傳時注意前後端最好都做下檢查,如文件的大小,文件的類型等等,我這裏就只做了後端的驗證。
文件上傳頁面代碼:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>文件上傳與下載</title> <noscript> <style> #main { display: none !important; } </style> <p align="center">您的瀏覽器禁止了JS,請先啓動腳本</p> </noscript> <script> function check() { var file = document.getElementById("file"); if (file.value == "") { alert("上傳的文件爲空") return false; } return true; } </script> </head> <body> <div id="main" style="width:500px; margin: 0 auto;"> <span style="color:red;">${msg}</span> <form action="" method="post" enctype="multipart/form-data" onsubmit="return check()"> <input type="file" name="file" id="file" multiple="multiple"><br> <input type="submit" value="上傳"> </form> </div> </body> </html>
在做限制文件上傳的大小時,注意不要在 SpringMVC 中直接限制,尤其是大文件(2M以上的),否則在上傳時 Tomcat 會關閉接收流,瀏覽器會失去響應。這個地方困擾的不止我一個人,這個 BUG 和 SpringMVC 無關,和 Tomcat 的一個屬性有關係,請看下圖,網上有人說 Tomcat7 就沒有這個問題,但這不是推薦的解決問題方式。
Tomcat文件上傳大小限制.jpg
經過一些研究,我的方案是用攔截器來做文件上傳的大小限制。當攔截器攔截文件超過設置的值時就拋出異常,在 Controller 中處理異常,這裏要在配置中延遲異常的解析時間。在攔截器的配置中,對攔截器的屬性做限制,在攔截器中獲取這個配置值,不要在攔截器中直接寫死。Controller 中捕獲這個異常,提示上傳文件超過了限制。
SpringMVC 中的配置:
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> <!--設置請求編碼--> <property name="defaultEncoding" value="UTF-8"/> <property name="uploadTempDir" value="WEB-INF/tmp"/> <!--設置允許單個上傳文件的最大值,不要在這裏配置--> <!--<property name="maxUploadSizePerFile" value="31457280"/>--> <!--延遲解析,在Controller中拋出異常--> <property name="resolveLazily" value="true"/> </bean> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/*upload*"/> <bean class="com.wenshixin.interceptor.FileUploadInterceptor"> <property name="maxSize" value="31457280"/> </bean> </mvc:interceptor> </mvc:interceptors>
攔截器:
public class FileUploadInterceptor implements HandlerInterceptor { private long maxSize; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { if (httpServletRequest != null && ServletFileUpload.isMultipartContent(httpServletRequest)) { ServletRequestContext servletRequestContext = new ServletRequestContext(httpServletRequest); long requestSize = servletRequestContext.contentLength(); if (requestSize > maxSize) { // 拋出異常 throw new MaxUploadSizeExceededException(maxSize); } } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } public void setMaxSize(long maxSize) { this.maxSize = maxSize; } }
Controller 中的異常處理方法:
@ExceptionHandler(MaxUploadSizeExceededException.class) public String handException(MaxUploadSizeExceededException e, HttpServletRequest request) { request.setAttribute("msg", "文件超過了指定大小,上傳失敗!"); return "fileupload"; }
SpringMVC 中使用 MultipartFile 對象來接收上傳的文件,通過這個對象可以得到文件的文件名和文件類型,通過 transferTo() 方法將文件寫入到磁盤上。文件上傳時,給文件重命名來防止上傳文件重名產生覆蓋,我這裏採取是 UUID值 + 文件名,中間用下劃線隔開。
Controller 中的文件上傳方法:
@PostMapping(value = "/fileupload") public String fileUpload(@RequestParam(value = "file") List<MultipartFile> files, HttpServletRequest request) { String msg = ""; // 判斷文件是否上傳 if (!files.isEmpty()) { // 設置上傳文件的保存目錄 String basePath = request.getServletContext().getRealPath("/upload/"); // 判斷文件目錄是否存在 File uploadFile = new File(basePath); if (!uploadFile.exists()) { uploadFile.mkdirs(); } for (MultipartFile file : files) { String originalFilename = file.getOriginalFilename(); if (originalFilename != null && !originalFilename.equals("")) { try { // 對文件名做加UUID值處理 originalFilename = UUID.randomUUID() + "_" + originalFilename; file.transferTo(new File(basePath + originalFilename)); } catch (IOException e) { e.printStackTrace(); msg = "文件上傳失敗!"; } } else { msg = "上傳的文件爲空!"; } } msg = "文件上傳成功!"; } else { msg = "沒有文件被上傳!"; } request.setAttribute("msg", msg); return "fileupload"; }
文件上傳的效果圖:
文件下載效果圖.gif
2.2 文件下載
下載頁面我使用了 Jquery 動態生成下載列表對 url 提前做了編碼處理,防止文件名中 # 號等特殊字符的干擾,並對顯示的文件名做了去除 UUID 值的處理,對 IE 瀏覽器也做了特殊的中文處理。
下載頁面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>文件上傳與下載</title> <script src="${pageContext.request.contextPath}/js/jquery-1.12.4.min.js"></script> <script> $(function(){ var targer = $("#main") $.ajax({ url: "fileList", dataType: "json", success: function (data) { data = JSON.parse(data) for (var i in data) { var a = $("<a></a><br>").text(data[i].substring(data[i].indexOf("_")+1)) a.attr("href", "${pageContext.request.contextPath}/download?filename="+encodeURIComponent(data[i])) targer.append(a) } } }) }) </script> </head> <body> <div id="main" style="width:500px; margin: 0 auto;"> </div> </body> </html>
Controller 中的下載方法:
@RequestMapping(value = "/download") public ResponseEntity<byte[]> fileDownload(String filename, HttpServletRequest request) throws IOException { String path = request.getServletContext().getRealPath("/upload/"); File file = new File(path + filename); // System.out.println("轉碼前" + filename); filename = this.getFilename(request, filename); // System.out.println("轉碼後" + filename); // 設置響應頭通知瀏覽器下載 HttpHeaders headers = new HttpHeaders(); // 將對文件做的特殊處理還原 filename = filename.substring(filename.indexOf("_") + 1); headers.setContentDispositionFormData("attachment", filename); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK); } // 根據不同的瀏覽器進行編碼設置,返回編碼後的文件名 public String getFilename(HttpServletRequest request, String filename) throws UnsupportedEncodingException { String[] IEBrowerKeyWords = {"MSIE", "Trident", "Edge"}; String userAgent = request.getHeader("User-Agent"); for (String keyword : IEBrowerKeyWords) { if (userAgent.contains(keyword)) { return URLEncoder.encode(filename, "UTF-8"); } } return new String(filename.getBytes("UTF-8"), "ISO-8859-1"); }
下載文件的效果圖(谷歌、火狐、IE、360瀏覽器):
文件上傳效果圖.gif
文件上傳下載是 Web 開發中很常見的功能,但是要想做好也並不容易,瀏覽器的兼容性要考慮,如果追求用戶體驗,還可以在上傳文件時給出進度條、AJAX實現頁面無刷新上傳,深感自己的前端水平還需要提高 ?,不說了學習去了。