文件上傳一個經常用到的功能,它有許多中實現的方案。
頁面表單 + RFC1897規范 + http協議上傳
頁面控件(flash/html5/activeX/applet) + RFC1897規范 + http協議上傳
頁面控件(flash/html5/activeX/applet) + 自定義數據規范 + http協議上傳
頁面控件(flash/html5/activeX/applet) + FTP協議上傳
頁面控件(flash/html5/activeX/applet) + 自定義協議
?
? 用apache common upload組件實際就是采用的“頁面表單 + RFC1897規范 + http協議上傳”實現方式,需要實現的技術點:
1. 多文件數據的提交
2. 文件數據包接收存儲功能
3. 文件數據上傳進度
4.?WEB頁面無刷新異步提交
?
?時序圖:
- 文件上傳時序圖
- 文件上傳進度獲取時序圖
實現思路:
1. 多文件數據的提交
在WEB頁面采用多個<input type="file">利用form表單進行文件提交
?
2. 文件數據包接收存儲功能
服務端采用servlet,利用apache common upload組件接收解析數據包,接收解析的過程中保存進度到session, 文件接收完畢后保存到指定目錄
?
3. 文件數據上傳進度
在WEB頁面在界面寫一個定時器,定時訪問服務器提供上傳進度獲取功能的servlet,獲取文件上傳進度信息
?
4. WEB頁面無刷新異步提交
利用iframe來實現WEB頁面無刷新異步上傳
?
關鍵代碼:
UploadFileServlet.java
Java代碼?
?
- package?com.test.servlet;??
- ??
- import?java.io.File;??
- import?java.io.IOException;??
- import?java.io.Writer;??
- import?java.util.Iterator;??
- import?java.util.List;??
- ??
- import?javax.servlet.ServletException;??
- import?javax.servlet.http.HttpServlet;??
- import?javax.servlet.http.HttpServletRequest;??
- import?javax.servlet.http.HttpServletResponse;??
- ??
- import?org.apache.commons.fileupload.FileItem;??
- import?org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException;??
- import?org.apache.commons.fileupload.disk.DiskFileItemFactory;??
- import?org.apache.commons.fileupload.servlet.FileCleanerCleanup;??
- import?org.apache.commons.fileupload.servlet.ServletFileUpload;??
- import?org.apache.commons.io.FileCleaningTracker;??
- import?org.apache.commons.io.FileUtils;??
- import?org.apache.commons.io.FilenameUtils;??
- import?org.apache.commons.io.IOUtils;??
- import?org.apache.commons.lang3.ArrayUtils;??
- import?org.apache.commons.logging.Log;??
- import?org.apache.commons.logging.LogFactory;??
- ??
- /**?
- ?*?文件上傳數據接收類?
- ?*??
- ?*?@author?chengqi?
- ?*?
- ?*/??
- public?class?UploadFileServlet?extends?HttpServlet?{??
- ??
- ????/**?日志對象*/??
- ????private?Log?logger?=?LogFactory.getLog(this.getClass());??
- ??
- ????private?static?final?long?serialVersionUID?=?1L;??
- ??
- ????/**?上傳目錄名*/??
- ????private?static?final?String?uploadFolderName?=?"uploadFiles";??
- ??
- ????/**?上傳臨時文件存儲目錄*/??
- ????private?static?final?String?tempFolderName?=?"tempFiles";??
- ??
- ????/**?上傳文件最大為30M*/???
- ????private?static?final?Long?fileMaxSize?=?30000000L;???
- ??
- ????/**?允許上傳的擴展名*/??
- ????private?static?final?String?[]?extensionPermit?=?{"txt",?"xls",?"zip"};??
- ??
- ????/**?統一的編碼格式*/??
- ????private?static?final?String?encode?=?"UTF-8";??
- ??
- ????@Override??
- ????protected?void?doPost(HttpServletRequest?request,?HttpServletResponse?response)?throws?ServletException,?IOException?{??
- ????????logger.info("UploadFileServlet#doPost()?start");??
- ????????try?{??
- ????????????String?curProjectPath?=?this.getServletContext().getRealPath("/");??
- ????????????String?saveDirectoryPath?=?curProjectPath?+?"/"?+?uploadFolderName;??
- ????????????String?tempDirectoryPath?=?curProjectPath?+?"/"?+?tempFolderName;??
- ????????????File?saveDirectory?=?new?File(saveDirectoryPath);??
- ????????????File?tempDirectory?=?new?File(tempDirectoryPath);??
- ????????????logger.debug("Project?real?path?["?+?saveDirectory.getAbsolutePath()?+?"]");??
- ????????????//上傳時產生的臨時文件的默認保存目錄??
- ????????????logger.debug("Temp?files?default?save?path?["?+?System.getProperty("java.io.tmpdir")?+?"]");??
- ????????????DiskFileItemFactory?factory?=?new?DiskFileItemFactory();??
- ????????????//DiskFileItemFactory中DEFAULT_SIZE_THRESHOLD=10240表示如果上傳文件大于10K則會產生上傳臨時文件??
- ????????????//上傳臨時文件的默認目錄為java.io.tmpdir中保存的路徑,根據操作系統的不同會有區別??
- ??????????????
- ????????????if(!tempDirectory.exists())?{??
- ????????????????tempDirectory.mkdir();??
- ????????????}??
- ????????????//重新設置臨時文件保存目錄??
- ????????????factory.setRepository(tempDirectory);??
- ??
- ????????????//設置文件清除追蹤器,文件上傳過程中產生的臨時文件會在??
- ????????????FileCleaningTracker?fileCleaningTracker?=?FileCleanerCleanup.getFileCleaningTracker(this.getServletContext());??
- ????????????factory.setFileCleaningTracker(fileCleaningTracker);??
- ??
- ????????????ServletFileUpload?upload?=?new?ServletFileUpload(factory);??
- ??
- ????????????//設置文件上傳進度監聽器??
- ????????????FileProcessListener?processListener?=?new?FileProcessListener(request.getSession());??
- ????????????upload.setProgressListener(processListener);??
- ??
- ????????????//?設置文件上傳的大小限制??
- ????????????upload.setFileSizeMax(fileMaxSize);??
- ??
- ????????????//?設置文件上傳的頭編碼,如果需要正確接收中文文件路徑或者文件名??
- ????????????//?這里需要設置對應的字符編碼,為了通用這里設置為UTF-8??
- ????????????upload.setHeaderEncoding(encode);??
- ??
- ????????????//解析請求數據包??
- ????????????List<FileItem>?fileItems?=?upload.parseRequest(request);??
- ????????????//遍歷解析完成后的Form數據和上傳文件數據??
- ????????????for?(Iterator<FileItem>?iterator?=?fileItems.iterator();?iterator.hasNext();)?{??
- ????????????????FileItem?fileItem?=?iterator.next();??
- ????????????????String?fieldName?=?fileItem.getFieldName();??
- ????????????????String?name?=?fileItem.getName();??
- ????????????????//如果為上傳文件數據??
- ????????????????if(!fileItem.isFormField())?{??
- ????????????????????logger.debug("fieldName["?+?fieldName?+?"]?fileName["?+?name?+?"]?");??
- ????????????????????if(fileItem.getSize()?>?0)?{??
- ????????????????????????String?fileExtension?=?FilenameUtils.getExtension(name);??
- ????????????????????????if(!ArrayUtils.contains(extensionPermit,?fileExtension))?{??
- ????????????????????????????throw?new?NoSupportExtensionException("No?Support?extension.");??
- ????????????????????????}??
- ????????????????????????String?fileName?=?FilenameUtils.getName(name);??
- ????????????????????????FileUtils.copyInputStreamToFile(fileItem.getInputStream(),???
- ????????????????????????????????new?File(saveDirectory,?fileName));??
- ????????????????????}??
- ????????????????}?else?{?//Form表單數據??
- ????????????????????String?value?=?fileItem.getString(encode);??
- ????????????????????logger.debug("fieldName["?+?value?+?"]?fieldValue["?+?fieldName?+?"]");??
- ????????????????}??
- ????????????}??
- ????????????responseMessage(response,?State.OK);??
- ????????}?catch(FileSizeLimitExceededException?e)?{???
- ????????????logger.error(e.getMessage(),?e);??
- ????????????responseMessage(response,?State.OVER_FILE_LIMIT);??
- ????????}?catch(NoSupportExtensionException?e)?{???
- ????????????logger.error(e.getMessage(),?e);??
- ????????????responseMessage(response,?State.NO_SUPPORT_EXTENSION);??
- ????????}?catch(Exception?e)?{??
- ????????????logger.error(e.getMessage(),?e);??
- ????????????responseMessage(response,?State.ERROR);??
- ????????}?finally?{??
- ????????????//清除上傳進度信息??
- ????????????request.getSession().removeAttribute("fileUploadProcess");??
- ????????}??
- ????????logger.info("UploadFileServlet#doPost()?end");???
- ????}??
- ??
- ????public?enum?State?{??
- ????????OK(200,?"上傳成功"),??
- ????????ERROR(500,?"上傳失敗"),??
- ????????OVER_FILE_LIMIT(501,?"超過上傳大小限制"),??
- ????????NO_SUPPORT_EXTENSION(502,?"不支持的擴展名");??
- ??
- ????????private?int?code;??
- ????????private?String?message;??
- ????????private?State(int?code,?String?message)?{??
- ????????????this.code?=?code;??
- ????????????this.message?=?message;??
- ????????}??
- ??
- ????????public?int?getCode()?{??
- ????????????return?code;??
- ????????}??
- ????????public?String?getMessage()?{??
- ????????????return?message;??
- ????????}??
- ??
- ????}??
- ??
- ????/**?
- ?????*?返回結果函數?
- ?????*?@param?response?
- ?????*?@param?state?
- ?????*/??
- ????private?void?responseMessage(HttpServletResponse?response,?State?state)?{??
- ????????response.setCharacterEncoding(encode);??
- ????????response.setContentType("text/html;?charset="?+?encode);??
- ????????Writer?writer?=?null;??
- ????????try?{??
- ????????????writer?=?response.getWriter();??
- ????????????writer.write("<script>");??
- ????????????writer.write("window.parent.fileUploadCallBack({\"code\":"?+?state.getCode()?+",\"message\":\""?+?state.getMessage()+?"\"});");??
- ????????????writer.write("</script>");??
- ????????????writer.flush();??
- ????????????writer.close();??
- ????????}?catch(Exception?e)?{??
- ????????????logger.error(e.getMessage(),?e);??
- ????????}?finally?{??
- ????????????IOUtils.closeQuietly(writer);??
- ????????}??
- ????}??
- ??
- ??
- }??
??
?
GetFileProcessServlet.java
Java代碼?
?
- package?com.test.servlet;??
- ??
- import?java.io.IOException;??
- import?java.io.Writer;??
- ??
- import?javax.servlet.ServletException;??
- import?javax.servlet.http.HttpServlet;??
- import?javax.servlet.http.HttpServletRequest;??
- import?javax.servlet.http.HttpServletResponse;??
- ??
- import?org.apache.commons.io.IOUtils;??
- import?org.apache.commons.logging.Log;??
- import?org.apache.commons.logging.LogFactory;??
- ??
- /**?
- ?*?文件上傳進度獲取Servlet?
- ?*??
- ?*?@author?chengqi?
- ?*?
- ?*/??
- public?class?GetFileProcessServlet?extends?HttpServlet?{??
- ??
- ????/**?日志對象*/??
- ????private?Log?logger?=?LogFactory.getLog(this.getClass());??
- ??
- ????private?static?final?long?serialVersionUID?=?1L;??
- ??
- ????@Override??
- ????protected?void?doGet(HttpServletRequest?request,?HttpServletResponse?response)??
- ????????????throws?ServletException,?IOException?{??
- ????????logger.info("GetFileProcessServlet#doGet?start");??
- ????????String?fileUploadPercent?=?(String)request.getSession().getAttribute("fileUploadProcess");??
- ????????Writer?writer?=?null;??
- ????????try?{??
- ????????????writer?=?response.getWriter();??
- ????????????logger.info("percent:"?+?fileUploadPercent);??
- ????????????IOUtils.write(fileUploadPercent?==?null???"0%"?:?fileUploadPercent,?writer);??
- ????????????writer.flush();??
- ????????????writer.close();??
- ????????}?catch(Exception?e)?{??
- ????????????logger.error(e.getMessage(),?e);??
- ????????}?finally?{??
- ????????????IOUtils.closeQuietly(writer);??
- ????????}??
- ????????logger.info("GetFileProcessServlet#doGet?end");??
- ????}??
- ??
- }??
?
FileProcessListener.java
Java代碼?
?
- package?com.test.servlet;??
- ??
- import?java.text.NumberFormat;??
- ??
- import?javax.servlet.http.HttpSession;??
- ??
- import?org.apache.commons.fileupload.ProgressListener;??
- import?org.apache.commons.logging.Log;??
- import?org.apache.commons.logging.LogFactory;??
- ??
- /**?
- ?*?文件進度監聽器?
- ?*??
- ?*?@author?chengqi?
- ?*?
- ?*/??
- public?class?FileProcessListener?implements?ProgressListener{??
- ??
- ????/**?日志對象*/??
- ????private?Log?logger?=?LogFactory.getLog(this.getClass());??
- ??
- ????private?HttpSession?session;??
- ??
- ????public?FileProcessListener(HttpSession?session)?{??
- ????????this.session?=?session;????
- ????}??
- ??????
- ??
- ????public?void?update(long?pBytesRead,?long?pContentLength,?int?pItems)?{??
- ????????double?readByte?=?pBytesRead;??
- ????????double?totalSize?=?pContentLength;??
- ????????if(pContentLength?==?-1)?{??
- ????????????logger.debug("item?index["?+?pItems?+?"]?"?+?pBytesRead?+?"?bytes?have?been?read.");??
- ????????}?else?{??
- ????????????logger.debug("item?index["?+?pItems?+?"]?"?+?pBytesRead?+?"?of?"?+?pContentLength?+?"?bytes?have?been?read.");??
- ????????????String?p?=?NumberFormat.getPercentInstance().format(readByte?/?totalSize);??
- ????????????session.setAttribute("fileUploadProcess",?p);??
- ????????}??
- ????}??
- ??
- }??
?
?
apacheUploadDemo.html
?
Html代碼?
?
- <!DOCTYPE?html?PUBLIC?"-//W3C//DTD?HTML?4.01?Transitional//EN"?"http://www.w3.org/TR/html4/loose.dtd">??
- <html>??
- <head>??
- ????<title>Apache?common實現基本文件上傳</title>??
- ????<meta?http-equiv="Content-Type"?content="text/html;?charset=UTF-8">??
- ????<script?type="text/javascript"?src="js/jquery/jquery-1.9.1.js"></script>??
- ????<script?type="text/javascript"?src="js/jquery/jquery.form.js"></script>??
- ????<script?type="text/javascript">??
- ??
- ????//定時器對象??
- ????var?uploadProcessTimer?=?null;??
- ??
- ????$(function?(){??
- ????????//綁定定時器開始操作到提交按鈕??
- ????????$('input[type=submit]').click(function?()?{??
- ????????????//啟動上傳進度查詢定時器??
- ????????????uploadProcessTimer?=?window.setInterval(getFileUploadProcess,?20);??
- ????????})??
- ????});??
- ??
- ????//獲取文件上傳進度??
- ????function?getFileUploadProcess()?{??
- ????????$.get('/upload/getFileProcessServlet',?function(data)?{??
- ????????????$('#fileUploadProcess').html(data);??
- ????????});??
- ????}??
- ??
- ????//上傳完成后,由iframe返回腳本自動調用??
- ????function?fileUploadCallBack(res)?{??
- ????????//清除定時器??
- ????????if(uploadProcessTimer)?{??
- ????????????window.clearInterval(uploadProcessTimer);??
- ????????}??
- ????????var?message?=?res['message'];??
- ????????var?code?=?res['code'];??
- ????????if(code?!=?200)?{??
- ????????????$('#fileUploadProcess').html('0%');??
- ????????}??
- ????????alert(message);??
- ????}??
- ??
- ????</script>??
- </head>??
- <body>??
- <h2>上傳文件1</h2>??
- ??
- 用戶信息:??<br/>??
- <form?id="testForm"?action="/upload/uploadServlet"?method="post"?enctype="multipart/form-data"?target="iframeUpload">??
- ????姓名:<input?name="name"?type="text">?<br/>??
- ????附件1:<input?name="file1"?type="file"?>?<br/>??
- ????附件2:<input?name="file2"?type="file"?>?<br/>??
- ????<br><br>??
- ????<input?type="submit"?value="提交"?><br/>??
- </form>??
- 上傳進度:<label?id="fileUploadProcess"></label>??
- <iframe?name="iframeUpload"?src=""?width="350"?height="35"?frameborder=0??SCROLLING="no"?style="display:NONE"></iframe>?????
- </body>??
- </html>??
?
總結:
雖然使用apache common upload組件實現了文件上傳,但是從上傳的效果來看,并不是一個很完美的解決方案。
有如下缺點:
1. 當有多個文件上傳時,無法知道單個文件的上傳進度,因為文件上傳消息中根本就沒有關于單個文件大小的信息
文件上傳消息
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 22 Apr 2014 07:45:45 GMT
POST /upload/uploadServlet HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Referer: http://localhost:8080/upload/apacheUploadDemo.html
Cookie: JSESSIONID=33498CE814284D67F957CA53D45F0174
Connection: keep-alive
Content-Length 2363
Content-Type multipart/form-data; boundary=---------------------------189163093917262
-----------------------------189163093917262
Content-Disposition: form-data; name="name"?
-----------------------------189163093917262
Content-Disposition: form-data; name="file1"; filename="New Text Document.txt" Content-Type: text/plain
文件數據
-----------------------------189163093917262
Content-Disposition: form-data; name="file2"; filename="New Text Document (2).txt" Content-Type: text/plain
文件數據
-----------------------------189163093917262--
??
?2. 瀏覽器必須將所有文件讀取完畢才開始上傳,并且是一次性提交所有的數據文件,在互聯網環境下,會http連接超時,大文件無法上傳成功。
?
3. 服務端判斷是否超過大小限制,是通過計算接收數據的累積字節數和限制大小比較,這種情況下,如果限制大小是30M,那么在服務端已經讀取了30M完成后才會拋出異常,多余的消耗的服務器的內存和硬盤空間
?
所以基于這些原因,頁面表單 + RFC1897規范 + http協議上傳 + 后臺apache common upload組件接收的這種解決方案,不適合解決WEB頁面一次多文件上傳,大文件上傳情況,比較適合一次單個小文件附件的情況,如:博客附件,登記照片上傳,預覽等情況。