酷炫的文件上传技术

1. JavaWeb:上传下载文件

http://blog.csdn.net/axi295309066/article/details/52984462

2. 课程概述

在Web应用系统开发中,文件上传功能是非常常用的功能,今天来主要讲讲JavaWeb中的文件上传功能的相关技术实现,并且随着互联网技术的飞速发展,用户对网站的体验要求越来越高,在文件上传功能的技术上也出现许多创新点,例如异步上传文件,拖拽式上传,黏贴上传,上传进度监控,文件缩略图,大文件断点续传,大文件秒传等等。

本课程需要的基础知识:

  • 了解基本的Http协议内容
  • 基本IO流操作技术
  • Servlet基础知识
  • javascript/jQuery技术基础知识

3. 文件上传的基础

对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,并且所有流数据都会随着Http请求携带到服务器端。所以,文件上传时的请求内容格式要能够基本看懂。

文件上传页面:

1
2
3
4
<form action="/itheimaUpload/UploadServlet" method="post" enctype="multipart/form-data">
请选择上传的文件:<input type="file" name="attach"/><br/>
<input type="submit" value="提交"/>
</form>

Http请求内容:

文件上传

文件上传

4. Java后台使用Servlet接收文件

如果使用Servlet获取上传文件的输入流然后再解析里面的请求参数是比较麻烦,所以一般后台选择采用Apache的开源工具common-fileupload这个文件上传组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//Java后台代码:Commons-fileUpload组件上传文件
public class UploadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//1.配置缓存
DiskFileItemFactory factory = new DiskFileItemFactory(1*1024*1024,new File("c:/tempFiles/"));
//2.创建ServleFileUpload对象
ServletFileUpload sfu = new ServletFileUpload(factory);
//解决文件名称中文问题
sfu.setHeaderEncoding("utf-8");
//3.解析
try {
List<FileItem> list = sfu.parseRequest(request);
//解析所有内容
if(list!=null){
for(FileItem item:list){
//判断是否为普通表单参数
if(item.isFormField()){
//普通表单参数
//获取表单的name属性名称
String fieldName = item.getFieldName();
//获取表单参数值
String value = item.getString("utf-8");
}else{
//文件
if(item.getName()!=null && !item.getName().equals("")) {
//保存到服务器硬盘
FileUtils.copyInputStreamToFile(item.getInputStream(), new File("c:/targetFiles/"+item.getName()));
item.delete();
}
}
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

5. 使用WebUploader上传组件

文件上传页面的前端我们可以选择使用一些比较好用的上传组件,例如百度的开源组件WebUploader,这个组件基本能满足文件上传的一些日常所需功能,如异步上传文件,拖拽式上传,黏贴上传,上传进度监控,文件缩略图,甚至是大文件断点续传,大文件秒传。

5.1 下载WebUpload组件

http://fex.baidu.com/webuploader/ 到WebUpload官网下载WebUpload包

WebUpload目录结构:

WebUpload目录结构

5.2 基本文件上传Demo(包含上传进度)

前端

1.1 在页面导入所需css,js

1
2
3
4
5
6
<link rel="stylesheet" type="text/css"
href="${pageContext.request.contextPath}/css/webuploader.css">
<script type="text/javascript"
src="${pageContext.request.contextPath }/js/jquery-1.10.2.min.js"></script>
<script type="text/javascript"
src="${pageContext.request.contextPath }/js/webuploader.js"></script>

1.2 编写上传页面标签

1
2
3
4
5
6
7
<!-- 上传div -->
<div id="uploader">
<!-- 显示文件列表信息 -->
<ul id="fileList"></ul>
<!-- 选择文件区域 -->
<div id="filePicker">点击选择文件</div>
</div>

1.3 编写webupload代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script type="text/javascript">
//1.初始化WebUpload,以及配置全局的参数
var uploader = WebUploader.create(
{
//flashk控件的地址
swf: "${pageContext.request.contextPath}/js/Uploader.swf",
//后台提交地址
server:"${pageContext.request.contextPath}/UploadServlet",
//选择文件控件的标签
pick:"#filePicker",
//自动上传文件
auto:true,
}
);
//2.选择文件后,文件信息队列展示
// 注册fileQueued事件:当文件加入队列后触发
// file: 代表当前选择的文件
uploader.on("fileQueued",function(file){
//追加文件信息div
$("#fileList").append("<div id='"+file.id+"' class='fileInfo'><span>"+file.name+"</span><div class='state'>等待上传...</div><span class='text'></span></div>");
});
//3.注册上传进度监听
//file: 正在上传的文件
//percentage: 当前进度的比例。最大为1.例如:0.2
uploader.on("uploadProgress",function(file,percentage){
var id = $("#"+file.id);
//更新状态信息
id.find("div.state").text("上传中...");
//更新上传百分比
id.find("span.text").text(Math.round(percentage*100)+"%");
});
//4.注册上传完毕监听
//file:上传完毕的文件
//response:后台回送的数据,以json格式返回
uploader.on("uploadSuccess",function(file,response){
//更新状态信息
$("#"+file.id).find("div.state").text("上传完毕");
});

2)后端Servlet代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DiskFileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload sfu = new ServletFileUpload(factory);
sfu.setHeaderEncoding("utf-8");
try {
List<FileItem> items = sfu.parseRequest(request);
for(FileItem item:items){
if(item.isFormField()){
//普通信息
}else{
//文件信息
//判断只有文件才需要进行保存处理
System.out.println("接收的文件名称:"+item.getName());
//拷贝文件到后台的硬盘
FileUtils.copyInputStreamToFile(item.getInputStream(), new File(serverPath+"/"+item.getName()));
System.out.println("文件保存成功");
}
}
} catch (FileUploadException e) {
e.printStackTrace();
}

5.3 生成图片缩略图

关键点:调用uploader.makeThumb()方法生成缩略图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uploader.on("fileQueued",function(file){
//追加文件信息div
$("#fileList").append("<div id='"+file.id+"' class='fileInfo'><img/><span>"+file.name+"</span><div class='state'>等待上传...</div><span class='text'></span></div>");
//制造图片缩略图:调用makeThumb()方法
//error: 制造缩略图失败
//src: 缩略图的路径
uploader.makeThumb(file,function(error,src){
var id = $("#"+file.id);
//如果失败,则显示“不能预览”
if(error){
id.find("img").replaceWith("不能预览");
}
//成功,则显示缩略图到指定位置
id.find("img").attr("src",src);
});
});

5.4 拖拽,黏贴上传

1)页面添加拖拽区域的div

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 上传div -->
<div id="uploader">
<!-- 文件拖拽区域 -->
<div id="dndArea">
<p>将文件直接拖拽到这里即可自动上传</p>
</div>
<!-- 显示文件列表信息 -->
<ul id="fileList"></ul>
<!-- 选择文件区域 -->
<div id="filePicker">点击选择文件</div>
</div>

2)在webuploader的全局配置参数添加拖拽功能的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1.初始化WebUpload,以及配置全局的参数
var uploader = WebUploader.create(
{
//flashk控件的地址
swf: "${pageContext.request.contextPath}/js/Uploader.swf",
//后台提交地址
server:"${pageContext.request.contextPath}/UploadServlet",
//选择文件控件的标签
pick:"#filePicker",
//自动上传文件
auto:true,
//开启拖拽功能,指定拖拽区域
dnd:"#dndArea",
//禁用页面其他地方的拖拽功能,防止页面直接打开文件
disableGlobalDnd:true
//开启黏贴功能
paste:"#uploader"
}
);

5.5 大文件分块上传

1)在webuploader全局参数中添加分块上传参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//1.初始化WebUpload,以及配置全局的参数
var uploader = WebUploader.create(
{
//flashk控件的地址
swf: "${pageContext.request.contextPath}/js/Uploader.swf",
//后台提交地址
server:"${pageContext.request.contextPath}/UploadServlet",
//选择文件控件的标签
pick:"#filePicker",
//自动上传文件
auto:true,
//开启拖拽功能,指定拖拽区域
dnd:"#dndArea",
//禁用页面其他地方的拖拽功能,防止页面直接打开文件
disableGlobalDnd:true,
//开启黏贴功能
paste:"#uploader",
//分块上传设置
//是否分块上传
chunked:true,
//每块文件大小(默认5M)
chunkSize:5*1024*1024,
//开启几个并发线程(默认3个)
threads:3,
//在上传当前文件时,准备好下一个文件
prepareNextFile:true
}
);

2)监控上传文件的三个时间点

添加以上三个配置后,会发现当文件超过5M时,webuploader自动把文件会分几个请求发送给后台

文件上传

每个分块请求,包含的信息:

文件上传

可以监听文件分块上传的三个重要的时间点。

文件上传

before-send-file : 在所有分块发送之前调用
before-send: 如果有分块,在每个分块发送之前调用
after-send-file: 在所有分块发送完成之后调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//5.监控文件上传的三个时间点(注意:该段代码必须放在WebUploader.create之前)
//时间点1::所有分块进行上传之前(1.可以计算文件的唯一标记;2.可以判断是否秒传)
//时间点2: 如果分块上传,每个分块上传之前(1.询问后台该分块是否已经保存成功,用于断点续传)
//时间点3:所有分块上传成功之后(1.通知后台进行分块文件的合并工作)
WebUploader.Uploader.register({
"before-send-file":"beforeSendFile",
"before-send":"beforeSend",
"after-send-file":"afterSendFile"
},{
//时间点1::所有分块进行上传之前调用此函数
beforeSendFile:function(){
//1.计算文件的唯一标记,用于断点续传和秒传
//2.请求后台是否保存过该文件,如果存在,则跳过该文件,实现秒传功能
},
//时间点2:如果有分块上传,则 每个分块上传之前调用此函数
beforeSend:function(){
//1.请求后台是否保存过当前分块,如果存在,则跳过该分块文件,实现断点续传功能
},
//时间点3:所有分块上传成功之后调用此函数
afterSendFile:function(){
//1.如果分块上传,则通过后台合并所有分块文件
}
});

before-send-file逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//利用md5File()方法计算文件的唯一标记符
//该函数接收一个deferred
beforeSendFile:function(file){
//创建一个deffered
var deferred = WebUploader.Deferred();
//1.计算文件的唯一标记,用于断点续传和秒传
(new WebUploader.Uploader()).md5File(file,0,5*1024*1024)
.progress(function(percentage){
$("#"+file.id).find("div.state").text("正在获取文件信息...");
})
.then(function(val){
uniqueFileTag = val;
$("#"+file.id).find("div.state").text("成功获取文件信息");
//只有文件信息获取成功,才进行下一步操作
deferred.resolve();
});
//alert(uniqueFileTag);
//2.请求后台是否保存过该文件,如果存在,则跳过该文件,实现秒传功能
//返回deffered
return deferred.promise();
}

before-send逻辑:

1
2
3
4
5
6
7
//向后台发送当前文件的唯一标记,用于后台创建保存分块文件的目录
beforeSend:function(){
//携带当前文件的唯一标记到后台,用于让后台创建保存该文件分块的目录
this.owner.options.formData.fileMd5 = fileMd5;
}

3)后台需要保存所有分块文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//为每个文件创建一个目录,并保存这个文件的所有分块文件
//判断是否已经分块上传
if(chunks!=null){
System.out.println("分块处理...");
//进行分块上传了
//建立一个临时目录,用于保存所有分块文件
File chunksDir = new File(serverPath+"/"+fileMd5);
if(!chunksDir.exists()){
chunksDir.mkdir();
}
if(chunk!=null){
//保存分块文件
File chunkFile = new File(chunksDir.getPath()+"/"+chunk);
FileUtils.copyInputStreamToFile(item.getInputStream(), chunkFile);
}

4)前台通知后台合并所有分块文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//前台通知后台合并文件
after-send-file逻辑:
afterSendFile:function(file){
//1.如果分块上传,则通过后台合并所有分块文件
//请求后台合并文件
$.ajax(
{
type:"POST",
url:"${pageContext.request.contextPath}/UploadCheckServlet?action=mergeChunks",
data:{
//文件唯一标记
fileMd5:fileMd5,
//文件名称
fileName:file.name
},
dataType:"json",
success:function(response){
alert(response.msg);
}
}
);
}
//后台合并所有分块文件
if("mergeChunks".equals(action)){
System.out.println("开始合并文件...");
//合并文件
String fileMd5 = request.getParameter("fileMd5");
String fileName = request.getParameter("fileName");
//读取目录里面的所有文件
File f = new File(serverPath+"/"+fileMd5);
File[] fileArray = f.listFiles(new FileFilter(){
//排除目录,只要文件
public boolean accept(File pathname) {
if(pathname.isDirectory()){
return false;
}
return true;
}
});
//转成集合,便于排序
List<File> fileList = new ArrayList<File>(Arrays.asList(fileArray));
//从小到大排序
Collections.sort(fileList, new Comparator<File>() {
public int compare(File o1, File o2) {
if(Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())){
return -1;
}
return 1;
}
});
File outputFile = new File(serverPath+"/"+fileName);
//创建文件
outputFile.createNewFile();
//输出流
FileChannel outChannel = new FileOutputStream(outputFile).getChannel();
//合并
FileChannel inChannel;
for(File file : fileList){
inChannel = new FileInputStream(file).getChannel();
inChannel.transferTo(0, inChannel.size(), outChannel);
inChannel.close();
//删除分片
file.delete();
}
//清除文件夹
File tempFile = new File(serverPath+"/"+fileMd5);
if(tempFile.isDirectory() && tempFile.exists()){
tempFile.delete();
}
//关闭流
outChannel.close();
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("{\"msg\":\"合并成功\"}");
}

5.6 大文件断点续传

在实现了分块上传的基础上,实现断点续传就非常简单了!!!

前端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//时间点2:如果有分块上传,则 每个分块上传之前调用此函数
//block:代表当前分块对象
beforeSend:function(block){
//1.请求后台是否保存过当前分块,如果存在,则跳过该分块文件,实现断点续传功能
var deferred = WebUploader.Deferred();
//请求后台是否保存完成该文件信息,如果保存过,则跳过,如果没有,则发送该分块内容
$.ajax(
{
type:"POST",
url:"${pageContext.request.contextPath}/UploadCheckServlet?action=checkChunk",
data:{
//文件唯一标记
fileMd5:fileMd5,
//当前分块下标
chunk:block.chunk,
//当前分块大小
chunkSize:block.end-block.start
},
dataType:"json",
success:function(response){
if(response.ifExist){
//分块存在,跳过该分块
deferred.reject();
}else{
//分块不存在或者不完整,重新发送该分块内容
deferred.resolve();
}
}
}
);
//携带当前文件的唯一标记到后台,用于让后台创建保存该文件分块的目录
this.owner.options.formData.fileMd5 = fileMd5;
return deferred.promise();
},

后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//检查该分块是否存在或者完整保存
private void checkChunk(HttpServletRequest request,
HttpServletResponse response) throws IOException,
FileNotFoundException {
System.out.println("checkChunk...");
String fileMd5 = request.getParameter("fileMd5");
String chunk = request.getParameter("chunk");
String chunkSize = request.getParameter("chunkSize");
File checkFile = new File(serverPath+"/"+fileMd5+"/"+chunk);
response.setContentType("text/html;charset=utf-8");
//检查文件是否存在,且大小是否一致
if(checkFile.exists() && checkFile.length()==Integer.parseInt(chunkSize)){
response.getWriter().write("{\"ifExist\":1}");
}else{
response.getWriter().write("{\"ifExist\":0}");
}
}

5.7 文件秒传

在所有分块请求之前,就已经可以进行实现秒传功能!!!

前端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
beforeSendFile:function(file){
//创建一个deffered
var deferred = WebUploader.Deferred();
//1.计算文件的唯一标记,用于断点续传和秒传
(new WebUploader.Uploader()).md5File(file,0,5*1024*1024)
.progress(function(percentage){
$("#"+file.id).find("div.state").text("正在获取文件信息...");
})
.then(function(val){
fileMd5 = val;
$("#"+file.id).find("div.state").text("成功获取文件信息");
//2.请求后台是否保存过该文件,如果存在,则跳过该文件,实现秒传功能
$.ajax(
{
type:"POST",
url:"${pageContext.request.contextPath}/UploadCheckServlet?action=fileCheck",
data:{
//文件唯一标记
fileMd5:fileMd5
},
dataType:"json",
success:function(response){
if(response.ifExist){
$("#"+file.id).find("div.state").text("秒传成功");
//如果存在,则跳过该文件,秒传成功
deferred.reject();
}else{
//继续上传
deferred.resolve();
}
}
}
);
});
//返回deffered
return deferred.promise();
},

后台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//检查文件的md5数据是否跟在数据库存在
private void fileCheck(HttpServletRequest request,
HttpServletResponse response) throws IOException,
FileNotFoundException {
String fileMd5 = request.getParameter("fileMd5");
//模拟数据库
Map<String,String> database = new HashMap<String,String>();
database.put("576018603f4091782b68b78af85704a1", "01.课程回顾.itcast");
response.setContentType("text/html;charset=utf-8");
if(database.containsKey(fileMd5)){
response.getWriter().write("{\"ifExist\":1}");
}else{
response.getWriter().write("{\"ifExist\":0}");
}
}

坚持原创技术分享,您的支持将鼓励我继续创作!