WEB文件上传之apache common upload使用(一)

时间:2023-12-29 13:48:14

文件上传一个经常用到的功能,它有许多中实现的方案。

页面表单 + 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页面无刷新异步提交

时序图:

  • 文件上传时序图

WEB文件上传之apache common upload使用(一)

  • 文件上传进度获取时序图
    WEB文件上传之apache common upload使用(一)

实现思路:

1. 多文件数据的提交

在WEB页面采用多个<input type="file">利用form表单进行文件提交

2. 文件数据包接收存储功能

服务端采用servlet,利用apache common upload组件接收解析数据包,接收解析的过程中保存进度到session, 文件接收完毕后保存到指定目录

3. 文件数据上传进度

在WEB页面在界面写一个定时器,定时访问服务器提供上传进度获取功能的servlet,获取文件上传进度信息

4. WEB页面无刷新异步提交

利用iframe来实现WEB页面无刷新异步上传

关键代码:

UploadFileServlet.java

  1. package com.test.servlet;
  2. import java.io.File;
  3. import java.io.IOException;
  4. import java.io.Writer;
  5. import java.util.Iterator;
  6. import java.util.List;
  7. import javax.servlet.ServletException;
  8. import javax.servlet.http.HttpServlet;
  9. import javax.servlet.http.HttpServletRequest;
  10. import javax.servlet.http.HttpServletResponse;
  11. import org.apache.commons.fileupload.FileItem;
  12. import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededException;
  13. import org.apache.commons.fileupload.disk.DiskFileItemFactory;
  14. import org.apache.commons.fileupload.servlet.FileCleanerCleanup;
  15. import org.apache.commons.fileupload.servlet.ServletFileUpload;
  16. import org.apache.commons.io.FileCleaningTracker;
  17. import org.apache.commons.io.FileUtils;
  18. import org.apache.commons.io.FilenameUtils;
  19. import org.apache.commons.io.IOUtils;
  20. import org.apache.commons.lang3.ArrayUtils;
  21. import org.apache.commons.logging.Log;
  22. import org.apache.commons.logging.LogFactory;
  23. /**
  24. * 文件上传数据接收类
  25. *
  26. * @author chengqi
  27. *
  28. */
  29. public class UploadFileServlet extends HttpServlet {
  30. /** 日志对象*/
  31. private Log logger = LogFactory.getLog(this.getClass());
  32. private static final long serialVersionUID = 1L;
  33. /** 上传目录名*/
  34. private static final String uploadFolderName = "uploadFiles";
  35. /** 上传临时文件存储目录*/
  36. private static final String tempFolderName = "tempFiles";
  37. /** 上传文件最大为30M*/
  38. private static final Long fileMaxSize = 30000000L;
  39. /** 允许上传的扩展名*/
  40. private static final String [] extensionPermit = {"txt", "xls", "zip"};
  41. /** 统一的编码格式*/
  42. private static final String encode = "UTF-8";
  43. @Override
  44. protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  45. logger.info("UploadFileServlet#doPost() start");
  46. try {
  47. String curProjectPath = this.getServletContext().getRealPath("/");
  48. String saveDirectoryPath = curProjectPath + "/" + uploadFolderName;
  49. String tempDirectoryPath = curProjectPath + "/" + tempFolderName;
  50. File saveDirectory = new File(saveDirectoryPath);
  51. File tempDirectory = new File(tempDirectoryPath);
  52. logger.debug("Project real path [" + saveDirectory.getAbsolutePath() + "]");
  53. //上传时产生的临时文件的默认保存目录
  54. logger.debug("Temp files default save path [" + System.getProperty("java.io.tmpdir") + "]");
  55. DiskFileItemFactory factory = new DiskFileItemFactory();
  56. //DiskFileItemFactory中DEFAULT_SIZE_THRESHOLD=10240表示如果上传文件大于10K则会产生上传临时文件
  57. //上传临时文件的默认目录为java.io.tmpdir中保存的路径,根据操作系统的不同会有区别
  58. if(!tempDirectory.exists()) {
  59. tempDirectory.mkdir();
  60. }
  61. //重新设置临时文件保存目录
  62. factory.setRepository(tempDirectory);
  63. //设置文件清除追踪器,文件上传过程中产生的临时文件会在
  64. FileCleaningTracker fileCleaningTracker = FileCleanerCleanup.getFileCleaningTracker(this.getServletContext());
  65. factory.setFileCleaningTracker(fileCleaningTracker);
  66. ServletFileUpload upload = new ServletFileUpload(factory);
  67. //设置文件上传进度监听器
  68. FileProcessListener processListener = new FileProcessListener(request.getSession());
  69. upload.setProgressListener(processListener);
  70. // 设置文件上传的大小限制
  71. upload.setFileSizeMax(fileMaxSize);
  72. // 设置文件上传的头编码,如果需要正确接收中文文件路径或者文件名
  73. // 这里需要设置对应的字符编码,为了通用这里设置为UTF-8
  74. upload.setHeaderEncoding(encode);
  75. //解析请求数据包
  76. List<FileItem> fileItems = upload.parseRequest(request);
  77. //遍历解析完成后的Form数据和上传文件数据
  78. for (Iterator<FileItem> iterator = fileItems.iterator(); iterator.hasNext();) {
  79. FileItem fileItem = iterator.next();
  80. String fieldName = fileItem.getFieldName();
  81. String name = fileItem.getName();
  82. //如果为上传文件数据
  83. if(!fileItem.isFormField()) {
  84. logger.debug("fieldName[" + fieldName + "] fileName[" + name + "] ");
  85. if(fileItem.getSize() > 0) {
  86. String fileExtension = FilenameUtils.getExtension(name);
  87. if(!ArrayUtils.contains(extensionPermit, fileExtension)) {
  88. throw new NoSupportExtensionException("No Support extension.");
  89. }
  90. String fileName = FilenameUtils.getName(name);
  91. FileUtils.copyInputStreamToFile(fileItem.getInputStream(),
  92. new File(saveDirectory, fileName));
  93. }
  94. } else { //Form表单数据
  95. String value = fileItem.getString(encode);
  96. logger.debug("fieldName[" + value + "] fieldValue[" + fieldName + "]");
  97. }
  98. }
  99. responseMessage(response, State.OK);
  100. } catch(FileSizeLimitExceededException e) {
  101. logger.error(e.getMessage(), e);
  102. responseMessage(response, State.OVER_FILE_LIMIT);
  103. } catch(NoSupportExtensionException e) {
  104. logger.error(e.getMessage(), e);
  105. responseMessage(response, State.NO_SUPPORT_EXTENSION);
  106. } catch(Exception e) {
  107. logger.error(e.getMessage(), e);
  108. responseMessage(response, State.ERROR);
  109. } finally {
  110. //清除上传进度信息
  111. request.getSession().removeAttribute("fileUploadProcess");
  112. }
  113. logger.info("UploadFileServlet#doPost() end");
  114. }
  115. public enum State {
  116. OK(200, "上传成功"),
  117. ERROR(500, "上传失败"),
  118. OVER_FILE_LIMIT(501, "超过上传大小限制"),
  119. NO_SUPPORT_EXTENSION(502, "不支持的扩展名");
  120. private int code;
  121. private String message;
  122. private State(int code, String message) {
  123. this.code = code;
  124. this.message = message;
  125. }
  126. public int getCode() {
  127. return code;
  128. }
  129. public String getMessage() {
  130. return message;
  131. }
  132. }
  133. /**
  134. * 返回结果函数
  135. * @param response
  136. * @param state
  137. */
  138. private void responseMessage(HttpServletResponse response, State state) {
  139. response.setCharacterEncoding(encode);
  140. response.setContentType("text/html; charset=" + encode);
  141. Writer writer = null;
  142. try {
  143. writer = response.getWriter();
  144. writer.write("<script>");
  145. writer.write("window.parent.fileUploadCallBack({\"code\":" + state.getCode() +",\"message\":\"" + state.getMessage()+ "\"});");
  146. writer.write("</script>");
  147. writer.flush();
  148. writer.close();
  149. } catch(Exception e) {
  150. logger.error(e.getMessage(), e);
  151. } finally {
  152. IOUtils.closeQuietly(writer);
  153. }
  154. }
  155. }

GetFileProcessServlet.java

  1. package com.test.servlet;
  2. import java.io.IOException;
  3. import java.io.Writer;
  4. import javax.servlet.ServletException;
  5. import javax.servlet.http.HttpServlet;
  6. import javax.servlet.http.HttpServletRequest;
  7. import javax.servlet.http.HttpServletResponse;
  8. import org.apache.commons.io.IOUtils;
  9. import org.apache.commons.logging.Log;
  10. import org.apache.commons.logging.LogFactory;
  11. /**
  12. * 文件上传进度获取Servlet
  13. *
  14. * @author chengqi
  15. *
  16. */
  17. public class GetFileProcessServlet extends HttpServlet {
  18. /** 日志对象*/
  19. private Log logger = LogFactory.getLog(this.getClass());
  20. private static final long serialVersionUID = 1L;
  21. @Override
  22. protected void doGet(HttpServletRequest request, HttpServletResponse response)
  23. throws ServletException, IOException {
  24. logger.info("GetFileProcessServlet#doGet start");
  25. String fileUploadPercent = (String)request.getSession().getAttribute("fileUploadProcess");
  26. Writer writer = null;
  27. try {
  28. writer = response.getWriter();
  29. logger.info("percent:" + fileUploadPercent);
  30. IOUtils.write(fileUploadPercent == null ? "0%" : fileUploadPercent, writer);
  31. writer.flush();
  32. writer.close();
  33. } catch(Exception e) {
  34. logger.error(e.getMessage(), e);
  35. } finally {
  36. IOUtils.closeQuietly(writer);
  37. }
  38. logger.info("GetFileProcessServlet#doGet end");
  39. }
  40. }

FileProcessListener.java

  1. package com.test.servlet;
  2. import java.text.NumberFormat;
  3. import javax.servlet.http.HttpSession;
  4. import org.apache.commons.fileupload.ProgressListener;
  5. import org.apache.commons.logging.Log;
  6. import org.apache.commons.logging.LogFactory;
  7. /**
  8. * 文件进度监听器
  9. *
  10. * @author chengqi
  11. *
  12. */
  13. public class FileProcessListener implements ProgressListener{
  14. /** 日志对象*/
  15. private Log logger = LogFactory.getLog(this.getClass());
  16. private HttpSession session;
  17. public FileProcessListener(HttpSession session) {
  18. this.session = session;
  19. }
  20. public void update(long pBytesRead, long pContentLength, int pItems) {
  21. double readByte = pBytesRead;
  22. double totalSize = pContentLength;
  23. if(pContentLength == -1) {
  24. logger.debug("item index[" + pItems + "] " + pBytesRead + " bytes have been read.");
  25. } else {
  26. logger.debug("item index[" + pItems + "] " + pBytesRead + " of " + pContentLength + " bytes have been read.");
  27. String p = NumberFormat.getPercentInstance().format(readByte / totalSize);
  28. session.setAttribute("fileUploadProcess", p);
  29. }
  30. }
  31. }

apacheUploadDemo.html

  1. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
  2. <html>
  3. <head>
  4. <title>Apache common实现基本文件上传</title>
  5. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  6. <script type="text/javascript" src="js/jquery/jquery-1.9.1.js"></script>
  7. <script type="text/javascript" src="js/jquery/jquery.form.js"></script>
  8. <script type="text/javascript">
  9. //定时器对象
  10. var uploadProcessTimer = null;
  11. $(function (){
  12. //绑定定时器开始操作到提交按钮
  13. $('input[type=submit]').click(function () {
  14. //启动上传进度查询定时器
  15. uploadProcessTimer = window.setInterval(getFileUploadProcess, 20);
  16. })
  17. });
  18. //获取文件上传进度
  19. function getFileUploadProcess() {
  20. $.get('/upload/getFileProcessServlet', function(data) {
  21. $('#fileUploadProcess').html(data);
  22. });
  23. }
  24. //上传完成后,由iframe返回脚本自动调用
  25. function fileUploadCallBack(res) {
  26. //清除定时器
  27. if(uploadProcessTimer) {
  28. window.clearInterval(uploadProcessTimer);
  29. }
  30. var message = res['message'];
  31. var code = res['code'];
  32. if(code != 200) {
  33. $('#fileUploadProcess').html('0%');
  34. }
  35. alert(message);
  36. }
  37. </script>
  38. </head>
  39. <body>
  40. <h2>上传文件1</h2>
  41. 用户信息:  <br/>
  42. <form id="testForm" action="/upload/uploadServlet" method="post" enctype="multipart/form-data" target="iframeUpload">
  43. 姓名:<input name="name" type="text"> <br/>
  44. 附件1:<input name="file1" type="file" > <br/>
  45. 附件2:<input name="file2" type="file" > <br/>
  46. <br><br>
  47. <input type="submit" value="提交" ><br/>
  48. </form>
  49. 上传进度:<label id="fileUploadProcess"></label>
  50. <iframe name="iframeUpload" src="" width="350" height="35" frameborder=0  SCROLLING="no" style="display:NONE"></iframe>
  51. </body>
  52. </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源码见附件