- 内容提要
- 作者简介
- 译者简介
- 前言
- HTTP
- Servlet 和 JSP
- 下载 Spring 或使用 STS 与 Maven/Gradle
- 手动下载 Spring
- 使用 STS 和 Maven/Gradle
- 下载 Spring 源码
- 本书内容简介
- 下载示例应用
- 第 1 章Spring 框架
- 第 2 章模型 2 和 MVC 模式
- 第 3 章Spring MVC 介绍
- 第 4 章基于注解的控制器
- 第 5 章数据绑定和表单标签库
- 第 6 章转换器和格式化
- 第 7 章验证器
- 第 8 章表达式语言
- 第 9 章JSTL
- 第 10 章国际化
- 第 11 章上传文件
- 第 12 章下载文件
- 第 13 章应用测试
- 附录 A Tomcat
- 附录 B Spring Tool Suite 和 Maven
- 附录 C Servlet
- 附录 D JavaServer Pages
- 附录 E 部署描述符
11.10 客户端上传
虽然 Servlet 3 中的文件上传特性使文件上传变得十分容易,只需在服务器端编程即可,但这对提升用户体验却毫无帮助。单独一个 HTML 表单并不能显示进度条,或者显示已经成功上传的文件数量。开发人员采用了各种不同的技术来改善用户界面,例如,单独用一个浏览器线程对服务器发出请求,以便报告上传进度,或者利用像 Java applets、Adobe Flash、Microsoft Silverlight 这样的第三方技术。
这些第三方技术可以工作,但都在一定程度上存在限制。今天 Java applets 和 Silverlight 几乎死了,Chrome 不再允许 applet 和 Silverlight,Microsoft 取代 Internet Explorer 的新浏览器 Edge 根本就不支持插件。
你仍然可以使用 Flash,因为 Chrome 仍然可以运行它,Edge 已经集成了它。然而,现在越来越多的人选择拥抱 HTML5。
HTML 5 在其 DOM 中添加了一个 File API。它允许访问本地文件。与 Java 小程序、Adobe Flash、Microsoft Silverlight 相比,HTML 5 似乎是针对客户端文件上传局限性的最佳解决方案。令人遗憾的是,在编写本书时,IE 9 尚未完全支持这个 API,但可以利用最新版的 Firefox、Chrome 和 Opera 浏览器来测试下面的例子。
为了证明 HTML 5 的威力,upload2 中的 html5.jsp 页面采用了 JavaScript 和 HTML 5 File API 来提供报告上传进度的进度条。upload2 应用程序中也复制了一份 MultipleUploadsServlet 类,用于在服务器中保存已上传的文件。但是,JavaScript 不在本书讨论范围之内,因此这里只做简单的说明。
简言之,我们关注的是 HTML 5 input 元素的 change 事件,当 input 元素的值发生改变时,就会触发它。本书还关注 HTML 5 在 XMLHttpRequest 对象中添加的 progress 事件。XMLHttpRequest 自然是 AJAX 的骨架。当异步使用 XMLHttpRequest 对象上传文件时,就会持续地触发 progress 事件,直到上传进度完成或取消,或者直到上传进度因为出错而中断。通过监听 progress 事件,可以轻松地监测文件上传操作的进度。
upload2 中的 Html5FileUploadController 类能够将已经上传的文件保存到应用程序目录的 file 目录下。清单 11.8 中的 UploadedFile 类展示了一个简单的 domain 类,它只包含一个属性。
清单 11.8 UploadedFile 的 domain 类
package domain;
import java.io.Serializable;
import org.springframework.web.multipart.MultipartFile;
public class UploadedFile implements Serializable {
private static final long serialVersionUID = 1L;
private MultipartFile multipartFile;
public MultipartFile getMultipartFile() {
return multipartFile;
}
public void setMultipartFile(MultipartFile multipartFile) {
this.multipartFile = multipartFile;
}
}
Html5FileUploadController 类如清单 11.9 所示。
清单 11.9 Html5FileUploadController 类
package controller;
import java.io.File;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import domain.UploadedFile;
@Controller
public class Html5FileUploadController {
private static final Log logger = LogFactory
.getLog(Html5FileUploadController.class);
@RequestMapping(value = "/html5")
public String inputProduct() {
return "Html5";
}
@RequestMapping(value = "/file_upload")
public void saveFile(HttpServletRequest servletRequest,
@ModelAttribute UploadedFile uploadedFile,
BindingResult bindingResult, Model model) {
MultipartFile multipartFile =
uploadedFile.getMultipartFile();
String fileName = multipartFile.getOriginalFilename();
try {
File file = new File(servletRequest.getServletContext()
.getRealPath("/file"), fileName);
multipartFile.transferTo(file);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Html5FileUploadController 中的 saveFile 方法将已经上传的文件保存到应用程序目录中的 file 目录下。
清单 11.10 所示的 html5.jsp 页面中包含的 JavaScript 代码允许用户选择多个文件,并且一键单击即可全部上传。这些文件本身将同时上传。
清单 11.10 html5.jsp 页面
<!DOCTYPE HTML>
<html>
<head>
<script>
var totalFileLength, totalUploaded, fileCount, filesUploaded;
function debug(s) {
var debug = document.getElementById('debug');
if (debug) {
debug.innerHTML = debug.innerHTML + '<br/>' + s;
}
}
function onUploadComplete(e) {
totalUploaded += document.getElementById('files').
files[filesUploaded].size;
filesUploaded++;
debug('complete ' + filesUploaded + " of " + fileCount);
debug('totalUploaded: ' + totalUploaded);
if (filesUploaded < fileCount) {
uploadNext();
} else {
var bar = document.getElementById('bar');
bar.style.width = '100%';
bar.innerHTML = '100% complete';
alert('Finished uploading file(s)');
}
}
function onFileSelect(e) {
var files = e.target.files; // FileList object
var output = [];
fileCount = files.length;
totalFileLength = 0;
for (var i=0; i<fileCount; i++) {
var file = files[i];
output.push(file.name, ' (',
file.size, ' bytes, ',
file.lastModifiedDate.toLocaleDateString(), ')'
);
output.push('<br/>');
debug('add ' + file.size);
totalFileLength += file.size;
}
document.getElementById('selectedFiles').innerHTML =
output.join('');
debug('totalFileLength:' + totalFileLength);
}
function onUploadProgress(e) {
if (e.lengthComputable) {
var percentComplete = parseInt(
(e.loaded + totalUploaded) * 100
/ totalFileLength);
var bar = document.getElementById('bar');
bar.style.width = percentComplete + '%';
bar.innerHTML = percentComplete + ' % complete';
} else {
debug('unable to compute');
}
}
function onUploadFailed(e) {
alert("Error uploading file");
}
function uploadNext() {
var xhr = new XMLHttpRequest();
var fd = new FormData();
var file = document.getElementById('files').
files[filesUploaded];
fd.append("multipartFile", file);
xhr.upload.addEventListener(
"progress", onUploadProgress, false);
xhr.addEventListener("load", onUploadComplete, false);
xhr.addEventListener("error", onUploadFailed, false);
xhr.open("POST", "file_upload");
debug('uploading ' + file.name);
xhr.send(fd);
}
function startUpload() {
totalUploaded = filesUploaded = 0;
uploadNext();
}
window.onload = function() {
document.getElementById('files').addEventListener(
'change', onFileSelect, false);
document.getElementById('uploadButton').
addEventListener('click', startUpload, false);
}
</script>
</head>
<body>
<h1>Multiple file uploads with progress bar</h1>
<div id='progressBar' style='height:20px;border:2px solid green'>
<div id='bar'
style='height:100%;background:#33dd33;width:0%'>
</div>
</div>
<form>
<input type="file" id="files" multiple/>
<br/>
<output id="selectedFiles"></output>
<input id="uploadButton" type="button" value="Upload"/>
</form>
<div id='debug'
style='height:100px;border:2px solid green;overflow:auto'>
</div>
</body>
</html>
html5.jsp 页面的用户界面中主要包含了一个名为 progressBar 的 div 元素、一个表单和另一个名为 debug 的 div 元素。也许你已经猜到了,progressBar div 用于展示上传进度,debug 用于调试信息。表单中有一个类型为 file 的 input 元素和一个按钮。
这个表单中有两点需要注意。第一,是标识为 files 的 input 元素,它有一个 multiple 属性,用于支持多文件选择。第二,这个按钮不是一个提交按钮。因此,单击它并不会提交表单。事实上,脚本是利用 XMLHttpRequest 对象来完成上传的。
下面来看 JavaScript 代码。我们假定读者已经具备一定的脚本语言知识。
执行脚本时,它做的第一件事就是为这 4 个变量分配空间:
var totalFileLength, totalUploaded, fileCount, filesUploaded;
totalFileLength 变量保存要上传的文件总长度。totalUploaded 是指目前已经上传的字节数。fileCount 中包含了要上传的文件数量。filesUploaded 表示已经上传的文件数量。
随后,当窗口完全下载后,便调用赋予 window.onload 的函数。
window.onload = function() {
document.getElementById('files').addEventListener(
'change', onFileSelect, false);
document.getElementById('uploadButton').
addEventListener('click', startUpload, false);
}
这段代码将 files input 元素的 change 事件映射到 onFileSelect 函数,将按钮的 click 事件映射到 startUpload。
每当用户从本地目录中修改了不同的文件时,都会触发 change 事件。与该事件相关的事件处理器只是在一个 output 元素中输出已选中的文件的名称和容量。下面是一个事件处理器的例子:
function onFileSelect(e) {
var files = e.target.files; // FileList object
var output = [];
fileCount = files.length;
totalFileLength = 0;
for (var i=0; i<fileCount; i++) {
var file = files[i];
output.push(file.name, ' (',
file.size, ' bytes, ',
file.lastModifiedDate.toLocaleDateString(), ')'
);
output.push('<br/>');
debug('add ' + file.size);
totalFileLength += file.size;
}
document.getElementById('selectedFiles').innerHTML =
output.join('');
debug('totalFileLength:' + totalFileLength);
}
当用户单击 Upload 按钮时,就会调用 startUpload 函数,并随之调用 uploadNext 函数。uploadNext 上传已选文件列表中的下一个文件。它首先创建一个 XMLHttpRequest 对象和一个 FormData 对象,并将接下来要上传的文件添加到它的后面。
var xhr = new XMLHttpRequest();
var fd = new FormData();
var file = document.getElementById('files').
files[filesUploaded];
fd.append("multipartFile", file);
随后,uploadNext 函数将 XMLHttpRequest 对象的 progress 事件添加到 onUploadProgress,并将 load 事件和 error 事件分别添加到 onUploadComplete 和 onUploadFailed。
xhr.upload.addEventListener(
"progress", onUploadProgress, false);
xhr.addEventListener("load", onUploadComplete, false);
xhr.addEventListener("error", onUploadFailed, false);
接下来,打开一个服务器连接,并发出 FormData。
xhr.open("POST", "file_upload");
debug('uploading ' + file.name);
xhr.send(fd);
在上传期间,会重复地调用 onUploadProgress 函数,让它有机会更新进度条。更新包括计算已经上传的总字节数比率,计算已选择文件的字节数,拓宽 progressBar div 元素里面的 div 元素。
function onUploadProgress(e) {
if (e.lengthComputable) {
var percentComplete = parseInt(
(e.loaded + totalUploaded) * 100
/ totalFileLength);
var bar = document.getElementById('bar');
bar.style.width = percentComplete + '%';
bar.innerHTML = percentComplete + ' % complete';
} else {
debug('unable to compute');
}
}
上传完成时,调用 onUploadComplete 函数。这个事件处理器会增加 totalUploaded,即已经完成上传的文件容量,并添加 filesUploaded 值。随后,它会查看已经选中的所有文件是否都已经上传完毕。如果是,则会显示一条消息,告诉用户文件上传已经成功完成。如果不是,则再次调用 uploadNext。为了便于阅读,将 onUploadComplete 函数重新复制到这里。
function onUploadComplete(e) {
totalUploaded += document.getElementById('files').
files[filesUploaded].size;
filesUploaded++;
debug('complete ' + filesUploaded + " of " + fileCount);
debug('totalUploaded: ' + totalUploaded);
if (filesUploaded < fileCount) {
uploadNext();
} else {
var bar = document.getElementById('bar');
bar.style.width = '100%';
bar.innerHTML = '100% complete';
alert('Finished uploading file(s)');
}
}
利用下面的 URL 可以对上述应用程序进行测试:
http://localhost:8080/upload2/html5
选择几个文件,并单击 Upload 按钮,将会看到一个进度条,以及上传文件的信息,屏幕截图如图 11.3 所示。
图 11.3 带进度条的文件上传
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论