文件上傳一個常常用到的功能,它有許多中實現的方案。javascript
頁面表單 + RFC1897規範 + http協議上傳html
頁面控件(flash/html5/activeX/applet) + RFC1897規範 + http協議上傳html5
頁面控件(flash/html5/activeX/applet) + 自定義數據規範 + http協議上傳java
頁面控件(flash/html5/activeX/applet) + FTP協議上傳jquery
頁面控件(flash/html5/activeX/applet) + 自定義協議apache
用apache common upload組件實際就是採用的「頁面表單 + RFC1897規範 + http協議上傳」實現方式,須要實現的技術點:瀏覽器
1. 多文件數據的提交服務器
2. 文件數據包接收存儲功能session
3. 文件數據上傳進度app
4. WEB頁面無刷新異步提交
時序圖:

- 文件上傳進度獲取時序圖

實現思路:
1. 多文件數據的提交
在WEB頁面採用多個<input type="file">利用form表單進行文件提交
2. 文件數據包接收存儲功能
服務端採用servlet,利用apache common upload組件接收解析數據包,接收解析的過程當中保存進度到session, 文件接收完畢後保存到指定目錄
3. 文件數據上傳進度
在WEB頁面在界面寫一個定時器,定時訪問服務器提供上傳進度獲取功能的servlet,獲取文件上傳進度信息
4. WEB頁面無刷新異步提交
利用iframe來實現WEB頁面無刷新異步上傳
關鍵代碼:
UploadFileServlet.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;
-
- 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";
-
-
- 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();
-
-
-
- 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);
-
-
-
- upload.setHeaderEncoding(encode);
-
-
- List<FileItem> fileItems = upload.parseRequest(request);
-
- 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 {
- 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;
- }
-
- }
-
-
- 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
- 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;
-
- 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
- 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;
-
- 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
- <!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頁面一次多文件上傳,大文件上傳狀況,比較適合一次單個小文件附件的狀況,如:博客附件,登記照片上傳,預覽等狀況。
Demo源碼見附件