带进度条的文件上传

  UploadServlet只实现了普通的文件上传,并附带普通文本域的提交。如果需要显示上传进度条,实时显示上传速度等,需要配合使用Ajax技术。这里仍然使用Apache的commons-fileupload实现文件上传。commons-fileupload从1.2版本开始支持上传监听器,能实时监听上传情况。

  工作原理

  实时显示上传进度的原理是服务器在处理上传文件的同时,将上传进度的信息例如文件总长度、已上传多少、传输速率等写入Session中。客户端浏览器利用Ajax技术再新开一个独立的线程从Session中获取上传进度信息,并实时显示。Ajax技术能够不刷新页面获取服务器数据。Session可看做是服务器内存,可用于存放少量的客户信息。Session详细用法请参看Session章节。

  本代码运行需要commons-fileupload-1.2.1.jar与commons-io-1.4.jar。

  上传进度条

  上传进度条用两个<div>标签实现,通过控制<div>的css属性就可以显示一个HTML版的进度条<div>标签连同css样式、JavaScript脚本、Ajax技术,可以实现非常丰富的效果,代码为:

<style type="text/css">
body, td, div {font-size: 12px; font-familly: 宋体; }
#progressBar {width: 400px; height: 12px; background: #FFFFFF; border: 1px solid #000000; padding: 1px; }
#progressBarItem {width: 30%; height: 100%; background: #FF0000; }
</style>
<div id="progressBar"><div id="progressBarItem"></div></div>

  进度条显示效果如下所示,要控制进度条的显示进度,只需要用JavaScript控制ID为progressBarItem的百分比宽度,比如30%、50%等。

  上传监听器

  commons-fileupload版本1.2支持上传监听,只需要实现一个监听器,并把它添加到上传组件上即可。监听器需要实现它的ProgressListener接口,如下:

package com.helloweenvsfei.servlet.upload;

import org.apache.commons.fileupload.ProgressListener;

public class UploadListener implements ProgressListener {

    private UploadStatus status;

    public UploadListener(UploadStatus status) {
this.status = status;
} public void update(long bytesRead, long contentLength, int items) {
status.setBytesRead(bytesRead);
status.setContentLength(contentLength);
status.setItems(items);
}
}

  ProgressListener接口只有一个方法:update(long bytesRead, long contentLength, int items)。参数bytesRead表示已经上传的字节数,contentLength表示上传文件的总长度(如果为-1则表示总长度未知),items表示正在上传第几个文件。

  添加了该监听器后,上传组件在上传文件时,会不断地回调该方法,回传这些数据。利用这些数字,就可以计算出文件上传的进度,用进度条实时显示出来。因此需要把这些数据保存起来。代码中把数据保存到了一个UploadStatus中。这是一个普通的Java Bean,代码如下:

package com.helloweenvsfei.servlet.upload;

public class UploadStatus {

    private long bytesRead;

    private long contentLength;

    private int items;

    private long startTime = System.currentTimeMillis();

    public long getBytesRead() {
return bytesRead;
} public void setBytesRead(long bytesRead) {
this.bytesRead = bytesRead;
} public long getContentLength() {
return contentLength;
} public void setContentLength(long contentLength) {
this.contentLength = contentLength;
} public int getItems() {
return items;
} public void setItems(int items) {
this.items = items;
} public long getStartTime() {
return startTime;
} public void setStartTime(long startTime) {
this.startTime = startTime;
} }

  UploadStatus中还记录了开始上传的时间,用于计算上传速率、估算上传总时间等。

  监听上传进度

  处理文件上传的Servlet为ProgressUploadServlet。与前面的UploadServlet类似,仍然使用ServletFileUpload类。监听上传过程需要为ServletFileUpload安装一个监听器,然后把存有上传进度信息的UploadStatus对象放进Session。上传文件使用的是POST方法,因此需要把代码写到doPost()方法中,代码如下:

package com.helloweenvsfei.servlet.upload;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload; public class ProgressUploadServlet extends HttpServlet { private static final long serialVersionUID = -4935921396709035718L; public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { // 上传状态
UploadStatus status = new UploadStatus(); // 监听器
UploadListener listener = new UploadListener(status); // 把 UploadStatus 放到 session 里
request.getSession(true).setAttribute("uploadStatus", status); // Apache 上传工具
ServletFileUpload upload = new ServletFileUpload(
new DiskFileItemFactory()); // 设置 listener
upload.setProgressListener(listener); try {
List itemList = upload.parseRequest(request); for (Iterator it = itemList.iterator(); it.hasNext();) {
FileItem item = (FileItem) it.next();
if (item.isFormField()) {
System.out.println("FormField: " + item.getFieldName()
+ " = " + item.getString());
} else {
System.out.println("File: " + item.getName()); // 统一 Linux 与 windows 的路径分隔符
String fileName = item.getName().replace("/", "\\");
fileName = fileName.substring(fileName.lastIndexOf("\\")); File saved = new File("C:\\upload_test", fileName);
saved.getParentFile().mkdirs(); InputStream ins = item.getInputStream();
OutputStream ous = new FileOutputStream(saved); byte[] tmp = new byte[1024];
int len = -1; while ((len = ins.read(tmp)) != -1) {
ous.write(tmp, 0, len);
} ous.close();
ins.close(); response.getWriter().println("已保存文件:" + saved);
}
}
} catch (Exception e) {
e.printStackTrace();
response.getWriter().println("上传发生错误:" + e.getMessage());
}
}
}

  现在使用ProgressUploadServlet来处理文件上传,就能获取到上传开始时间、已上传的字节数、总的字节数、正在上传第几个文件等信息了。只不过这些信息还是保存在Session里,需要再写一个程序读取出来。

  读取上传进度

  上传进度保存在Session中的uploadStatus属性中,从该属性中获取UploadStatus对象,并将上传进度信息读取出来。上传文件只会使用ProgressUploadServlet的doPost()方法而没有占用doGet()方法,因此这里使用doGet()方法来读取上传进度。也就是说,如果以POST方式访问ProgressUploadServlet,会执行上传代码:如果以GET方式访问ProgressUploadServlet,会执行读取上传进度的代码。

  根据上传开始时间、已上传的字节数、总的字节数等原始数据,可以计算出已传输时间、传输速率、传输总时间、剩余时间、已完成百分比等。这些数据最终要被Ajax程序读取、以进度条形式显示,因此最后将它们用分隔符"||"分开,便于程序处理,代码如下:

public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { response.setHeader("Cache-Control", "no-store");
response.setHeader("Pragrma", "no-cache");
response.setDateHeader("Expires", 0); UploadStatus status = (UploadStatus) request.getSession(true)
.getAttribute("uploadStatus"); if (status == null) {
response.getWriter().println("没有上传信息");
return;
} long startTime = status.getStartTime();
long currentTime = System.currentTimeMillis(); // 已传输的时间 单位:s
long time = (currentTime - startTime) / 1000 + 1; // 传输速度 单位:byte/s
double velocity = ((double) status.getBytesRead()) / (double) time; // 估计总时间 单位:s
double totalTime = status.getContentLength() / velocity; // 估计剩余时间 单位:s
double timeLeft = totalTime - time; // 已完成的百分比
int percent = (int) (100 * (double) status.getBytesRead() / (double) status
.getContentLength()); // 已完成数 单位:M
double length = ((double) status.getBytesRead()) / 1024 / 1024; // 总长度 单位:M
double totalLength = ((double) status.getContentLength()) / 1024 / 1024; // 格式:百分比||已完成数(M)||文件总长度(M)||传输速率(K)||已用时间(s)||估计总时间(s)||估计剩余时间(s)||正在上传第几个文件
String value = percent + "||" + length + "||" + totalLength + "||"
+ velocity + "||" + time + "||" + totalTime + "||" + timeLeft
+ "||" + status.getItems(); response.getWriter().println(value);
}

  现在上传文件使用快捷键Ctrl+N新开一个子窗口访问ProgressUploadServlet,就可以看到文字版的上传进度。不断的刷新,就能不断地更新。不过手工查看上传进度太笨拙了,下一步要写一个Ajax程序,自动获取该Servlet返回的数据、将数据显示到进度条上,并且每一秒钟自动更新一次。

  显示上传进度

  上传文件时,如果不对表单做特别处理,提交表单后会转到另一个页面,造成页面的刷新。而且新页面显示之前,浏览器会因等待而显示白屏。如果 上传的文件很大,白屏时间会很长。因此需要对表单做一些特别处理,使提交表单后原页面的内容不变同时显示进度条,直到文件上传结束,从而避免出现白屏。方法是更改Form的target属性,代码如下:

<iframe name=upload_iframe width=0 height=0></iframe>

<form action="servlet/ProgressUploadServlet" method="post" enctype="multipart/form-data" target="upload_iframe" onsubmit="showStatus(); ">

<input type="file" name="file1" style="width: 350px; "> <br />
<input type="file" name="file2" style="width: 350px; "> <br />
<input type="file" name="file3" style="width: 350px; "> <br />
<input type="file" name="file4" style="width: 350px; "> <input type="submit"
value=" 开始上传 " id="btnSubmit"></form>

  target属性默认为_self。如果target为默认值,则提交后的新页面会在当前窗口显示,造成当前窗口短暂地白屏。在页面内添加一个隐藏的iframe(通过指定iframe的宽高为0实现隐藏),把target属性指定为该iframe,则提交后的新页面会在iframe内显示,iframe内会暂时白屏。因为iframe是隐藏的,所以上传文件的时候当前页面看不出任何变化。

  注意上传文件的表单要指定method="POST"与enctype="multipart/form-data"。另外为表单添加了onsubmit="showStatus()"事件,表单提交时会执行页面内的showStatus()方法。这是一段javaScript程序代码。该方法显示进度条,并用Ajax读取Session里保存的上传进度,实时刷新进度条。showStatus()方法的代码如下:

function showStatus(){
_finished = false;
$('status').style.display = 'block';
$('progressBarItem').style.width = '1%';
$('btnSubmit').disabled = true; setTimeout("requestStatus()", 1000);
}

  Ajax能够不刷新页面改变页面的内容。它的原理是创建一个request,用这个request获取其他页面的内容,并显示到页面上。因为没有在浏览器中输入新的地址,所以页面不会有刷新、拉动等。

  Ajax的核心就是这个request,学名叫XMLHttpRequest,需要浏览器支持。目前所有主流的浏览器都能提供XMLHttpRequest。XMLHttpRequest的用法本书后面章节有详细介绍。progressUpload.jsp所有代码如下:

<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<style type="text/css">
body, td, div {font-size: 12px; font-familly: 宋体; }
#progressBar {width: 400px; height: 12px; background: #FFFFFF; border: 1px solid #000000; padding: 1px; }
#progressBarItem {width: 30%; height: 100%; background: #FF0000; }
</style>
</head> <body> <iframe name=upload_iframe width=0 height=0></iframe> <form action="servlet/ProgressUploadServlet" method="post" enctype="multipart/form-data" target="upload_iframe" onsubmit="showStatus(); "> <input type="file" name="file1" style="width: 350px; "> <br />
<input type="file" name="file2" style="width: 350px; "> <br />
<input type="file" name="file3" style="width: 350px; "> <br />
<input type="file" name="file4" style="width: 350px; "> <input type="submit"
value=" 开始上传 " id="btnSubmit"></form> <div id="status" style="display: none; ">
上传进度条:
<div id="progressBar"><div id="progressBarItem"></div></div>
<div id="statusInfo"></div>
</div> <br/>
<br/>
<br/>
<br/>
<br/> <script type="text/javascript"> var _finished = true; function $(obj){
return document.getElementById(obj);
} function showStatus(){
_finished = false;
$('status').style.display = 'block';
$('progressBarItem').style.width = '1%';
$('btnSubmit').disabled = true; setTimeout("requestStatus()", 1000);
} function requestStatus(){ if(_finished) return; var req = createRequest(); req.open("GET", "servlet/ProgressUploadServlet");
req.onreadystatechange=function(){callback(req);}
req.send(null); setTimeout("requestStatus()", 1000);
} function createRequest()
{
if(window.XMLHttpRequest)//ns
{
return new XMLHttpRequest();
}else//IE
{
try{
return new ActiveXObject("Msxml2.XMLHTTP");
}catch(e){
return new ActiveXObject("Microsoft.XMLHTTP");
}
}
return null;
}
function callback(req){ if(req.readyState == 4) {
if(req.status != 200){
_debug("发生错误。 req.status: " + req.status + "");
return;
} _debug("status.jsp 返回值:" + req.responseText); var ss = req.responseText.split("||"); // 格式:百分比||已完成数(M)||文件总长度(M)||传输速率(K)||已用时间(s)||估计总时间(s)||估计剩余时间(s)||正在上传第几个文件
$('progressBarItem').style.width = '' + ss[0] + '%';
$('statusInfo').innerHTML = '已完成百分比: ' + ss[0] + '% <br />已完成数(M): ' + ss[1] + '<br/>文件总长度(M): ' + ss[2] + '<br/>传输速率(K): ' + ss[3] + '<br/>已用时间(s): ' + ss[4] + '<br/>估计总时间(s): ' + ss[5] + '<br/>估计剩余时间(s): ' + ss[6] + '<br/>正在上传第几个文件: ' + ss[7]; if(ss[1] == ss[2]){
_finished = true;
$('statusInfo').innerHTML += "<br/><br/><br/>上传已完成。";
$('btnSubmit').disabled = false;
}
}
}
function _debug(obj){
var div = document.createElement("DIV");
div.innerHTML = "[debug]: " + obj;
document.body.appendChild(div);
} </script> </body>
</html>

  运行效果如下:

  文件上传进度条是很人性化的设计,尤其是文件很大的情况。还可以把进度条放到一个可移动的层上,并美化一下,使之看起来更像对话框。

Servlet学习记录4的更多相关文章

  1. Servlet学习记录3

    提交表单信息 Web程序的任务是实现服务器与客户端浏览器之间的信息交互.客户端提交的信息可能来自表单里的文本框,密码框,选择框,单选按钮,复选框以及文件域.这些表单信息被以参数形式提交到了服务器.Se ...

  2. Servlet学习记录2

    读取web.xml参数 上篇文章ImageServlet里只设置了JPG,GIF,DOC类型文件的Content-Type.如果这时候需求变化了,需要增加Excel文件格式的Content-Type, ...

  3. servlet学习记录:Servlet中的service()方法

    Servlet的生存时间是由init,service,destory方法构成,这里分析一下service这个方法 Servlet接口中定义了一个service()方法,而我们一般是使用HttpServ ...

  4. Servlet学习记录

    个人认为servlet属于一种控制程序,可以处理浏览器的请求并做出对应的回应.我们经常使用的是让一个类去继承HttpServlet,然后在doget或者dopost里面写东西. 目前我个人常在doge ...

  5. 我的Spring学习记录(五)

    在我的Spring学习记录(四)中使用了注解的方式对前面三篇做了总结.而这次,使用了用户登录及注册来对于本人前面四篇做一个应用案例,希望通过这个来对于我们的Spring的使用有一定的了解. 1. 程序 ...

  6. Spring 学习记录8 初识XmlWebApplicationContext(2)

    主题 接上文Spring 学习记录7 初识XmlWebApplicationContext refresh方法 refresh方法是定义在父类AbstractApplicationContext中的. ...

  7. HTTP学习记录

    title: HTTP学习记录 toc: true date: 2018-09-21 20:40:48 HTTP协议,HyperText Transfer Protocol,超文本传输协议,是因特网上 ...

  8. 我的Spring Boot学习记录(二):Tomcat Server以及Spring MVC的上下文问题

    Spring Boot版本: 2.0.0.RELEASE 这里需要引入依赖 spring-boot-starter-web 这里有可能有个人的误解,请抱着怀疑态度看. 建议: 感觉自己也会被绕晕,所以 ...

  9. 学习记录-java基础部分(一)

    学习记录-java基础部分(一) 参考:GitHub上的知名项目:javaGuide : https://github.com/Snailclimb/JavaGuide/blob/master/doc ...

随机推荐

  1. 芯灵思Sinlinx A64 Linux&qt编译安装

    开发平台 芯灵思Sinlinx A64 内存: 1GB 存储: 4GB 详细参数 https://m.tb.cn/h.3wMaSKm 开发板交流群 641395230 前提条件搭建好CentOS环境 ...

  2. Nginx failing to load CSS and JS files (MIME type error)

    Nginx failing to load CSS and JS files (MIME type error) Nginx加载静态文件失败的解决方法(MIME type错误) 上线新的页面,需要在n ...

  3. Ansible 安装与配置(一)

    公司大概有200多云主机需要进行管理,但是如果通过手工管理费时还累最终结果也容易出错,所以考虑通过自动化的方式来管理云主机,目前开源的自动化工具,大家用的比较多的有Ansible和Saltstack这 ...

  4. 开发系统app所遇到的问题及解决

    1. 在源码环境中编译app时(使用mmm编译需要根据app写好Android.mk文件)遇到如下问题 error: Resource at colorPrimary appears in overl ...

  5. java大数据量调优

    从总体上来看,对于大型网站,比如门户网站,在面对大量用户访问.高并发请求方面,基本的解决方案集中在这样几个环节:1.首先需要解决网络带宽和Web请求的高并发,需要合理的加大服务器和带宽的投入,并且需要 ...

  6. ES timeout 的一些笔记

    首先解释下Timeout的作用: 比如,一个search,可能要10分钟才能搜完,那么,es就会等10分钟,直到结果出来.然而,在某些场景下,客户是等不了10分钟的.比如,淘宝搜东西的时候,客户宁可等 ...

  7. 用户层APC队列使用

    一 参考 https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-que ...

  8. 使用svn创建分支!

    1 在主分支上 右键svn---选中 branch/Tag选项 2,填写新分支目录之后 点击 ok键 3,在新创建的分支目录 右键 --> Chenckout下  就可以把代码拉下来了 4.更新 ...

  9. ct

    b80e00u9dxwpqw7bt98rm5zmlxt08cxs A3WKXKBHWDUOEOP3EVJA2YRM6JSZPJWGTCQ5BSYAWI4GMSIXOAT2IQ

  10. day72Django之ORM

    Django框架之ORM(day72)一 ORM即Object Relational Mapping,全称对象关系映射. 1 不用写sql,不会sql的人也可以写程序 2 开发效率高 3 可能sql的 ...