javaWeb实现多文件上传以及打包下载

序言

今天学习了JavaWeb中对文件的上传和下载,在以前我们都是借助工具包来实现,主要有Apache的commoms-fileupload和commons-io等主流的jar包。3.0之后Tomcat集成了这些工具,所以我们可以在不用导入外部类库的情况下轻松使用文件上传功能。

概览

这个案例主要包含了四个类,文件上传PartFileUpload.java、列出文件ListFilesServlet.java、单文件下载DownloadOneServlet.java、多文件打包下载DownloadZipServlet.java 四个Servlet。其中较为复杂的为列出文件和多文件打包下载两个类,详细的实现步骤我已经用注释写在代码里了。

实现思路

  1. 为了简单的实现功能,我们暂时不考虑复杂的处理例如:文件打散存储、给文件名加上uuid等
  2. 上传文件使用了Servlet3.0以后支持的Part类,大大减少了代码量。

列出文件及目录

  1. 创建两个map,一个用于存储文件名–文件名的hashCode,另一个存储hashCode–文件真实路径,这样我们就可以通过文件名找到文件的路径。(目录同理)
  2. 文件和目录各自有两个map,为了jsp可以对出文件和目录做出不同的处理。
  3. 每次用户访问ListFilesServlet,我们就会根据请求的参数找到需要遍历的路径,然后遍历这个路径,将里边的目录/文件的信息放入map中。
  4. 将map作为Attribute发送给jsp。

多文件打包下载

  1. jsp中使用了checkbox,其value为文件的hashcode,如果用户多选,我们就可以获取到存着hashCode的数组。
  2. 遍历hashCode数组,从另一个map中根据hashCode拿到文件的真实路径。
  3. 使用FileInputStream,ZipOutPutStream,将每一个文件添加到压缩输出流。
  4. 设置对应的content-type以及content-disposition,告诉浏览器以下载的形式打开。
  5. 将压缩输出流返回个客户端。

文件上传和单文件下载

这两个类的实现较为简单,大家直接看代码和注释即可。

效果图


例子中可能需要用到的jar包

commons-io

源码

PartFileUpload.java

源码中关于Part类的说明:This class represents a part as uploaded to the server as part of a multipart/form-data request body.

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
import java.io.File;
import java.io.IOException;
import java.util.Collection;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;

/**
* Servlet3.0
* 使用Par方式上传,大大减少了代码量
* @author HouHuashi
*
*/
@MultipartConfig //注解,标识这是一个可以下载文件的Servlet,里面可以配置属性,例如sizeThreashold,maxSize等参数
@WebServlet("/part")
public class PartFileUpload extends HttpServlet {
private static final long serialVersionUID = 1L;

public PartFileUpload() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 处理文件名的中文乱码问题
request.setCharacterEncoding("utf-8");
// 拿到请求中Part的集合
Collection<Part> parts = request.getParts();
String storePath=getServletContext().getRealPath("/WEB-INF/upload");

for(Part p:parts) {
// 这个方法获取到文件名,如果不是文件内容,就返回空
String fName = p.getSubmittedFileName();
if(fName==null) {
// 如果不是文件内容,打印表单的内容
System.out.println("form data : "+IOUtils.copy(p.getInputStream(), System.out));
}else {
// 由于浏览器差异,有的浏览器上传的文件名可能带有路径信息
// 使用commons-io的工具类格式化文件名
fName=FilenameUtils.getName(fName);
//使用Part的write可以直接将文件内容写出到文件
p.write(storePath+File.separator+fName);
}
}
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

}

ListFilesServlet.java

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 列出文件列表的Servlt 支持目录下同时有文件夹以及文件
*
* 使用四个map存放不同的内容
* fileMap存放文件名和文件名的hashCode
* pathMap存放文件名的hashCode和文件的真实路径
* folderMap存放目录的名字和目录名字的hashCode
* fpathMap存放目录名的hashCode和目录的真实路径
*
* 至于为什么要使用四个map:避免了向用户展示出服务器的真实路径结构, 同时也简化了请求的参数,避免了参数中关于字符的其他问题
*
* @author HouHuashi
*
*/
@WebServlet("/ListFilesServlet")
public class ListFilesServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

public ListFilesServlet() {
super();
}

// key:file uuid , value: file path
Map<String, String> pathMap = new HashMap<String, String>();

// key:folder uuid ,value: folder path
Map<String, String> fpathMap = new HashMap<>();

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

// key:fileName value: file uuid 存放文件名和uuid,用于给jsp显示用
Map<String, String> fileMap = new HashMap<String, String>();

// key:folderName value:folder uuid,存放目录的名字和uuid,用于给jsp显示用
Map<String, String> folderMap = new HashMap<>();

String uuid = (String) request.getParameter("uuid");
String path;
if (uuid == null) {
// 如果用户访问没带任何参数,默认是我们文件上传的根目录
path = "WEB-INF/upload";
// 考虑到每次访问servlet生成的uuid都不一样,这里设置返回根目录就清空一下map
// 已改进:可以使用hash算法代替让每个目录的id是不变的
// pathMap=new HashMap<>();
// fpathMap=new HashMap<>();
} else {
// 如果访问的不是根目录,那么就拼接一下
path = "WEB-INF" + File.separator + fpathMap.get(uuid);
}

// 我们在jsp中规定好了,只有是文件夹的超连接才会执行这个servlet,所以此处直接按文件夹遍历
File folder = new File(getServletContext().getRealPath(File.separator + path));
System.out.println("folder:" + folder.getPath());
File[] files = folder.listFiles();
putFiles(files, fileMap, pathMap, folderMap, fpathMap);
request.setAttribute("fileMap", fileMap);
request.setAttribute("folderMap", folderMap);
request.getSession().setAttribute("pathMap", pathMap);
request.getRequestDispatcher("list.jsp").forward(request, response);
}


/**
* 将文件或文件夹的信息存进我们的四个map里
* @param files
* @param fileMap 存放文件名和文件名的hashCode
* @param pathMap 存放文件名的hashCode和文件的真实路径
* @param folderMap 存放目录的名字和目录名字的hashCode
* @param fpathMap 存放目录名的hashCode和目录的真实路径
*/
private void putFiles(File[] files, Map<String, String> fileMap, Map<String, String> pathMap,
Map<String, String> folderMap, Map<String, String> fpathMap) {
for (File file : files) {
// String uuid = UUID.randomUUID().toString();
// 使用hashcode不会造成每次都向map里放入新的键值对,节省内存空间
String uuid = file.getName().hashCode() + "";

if (file.isFile()) {
// 如果是文件那就把他的信息放进file的map里
fileMap.put(file.getName(), uuid);
pathMap.put(uuid, file.getPath());
} else {
folderMap.put(file.getName(), uuid);
// 截取一下目录的路径,只保留upload以后的内容,例如:upload/folder1
fpathMap.put(uuid, file.getPath().substring(file.getPath().lastIndexOf("upload")));
}
System.out.println(file.getPath());
}
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

}

DownloadOneServlet.java

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
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.io.IOUtils;

/**
* 单文件的下载,比较简单
* @author HouHuashi
*
*/
@WebServlet("/DownloadOneServlet")
public class DownloadOneServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

public DownloadOneServlet() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String fileId = request.getParameter("fileId");
HttpSession session = request.getSession();
// 可以从我们之前设好的map里根据文件的hashcode拿到它的真实路径
Map<String,String> pathMap =(Map<String, String>) session.getAttribute("pathMap");
String path = pathMap.get(fileId);

File file=new File(path);
FileInputStream fis=new FileInputStream(file);
response.setContentType("application/ocete-stream");
response.setHeader("content-disposition", "attachment;filename="+file.getName());
IOUtils.copy(fis, response.getOutputStream());
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}

}

PDownloadZipServlet.java

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
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.tomcat.util.http.fileupload.IOUtils;

/**
* 处理批量下载请求的Servlet
* @author HouHuashi
*
*/
@WebServlet("/DownloadZipServlet")
public class DownloadZipServlet extends HttpServlet {
private static final long serialVersionUID = 1L;

public DownloadZipServlet() {
super();
}

protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 为响应设置contenttype 提示浏览器以字节流的形式解析
response.setContentType("application/octet-stream");
// 告诉浏览器以下载的形式打开,并指定一个文件名
response.setHeader("content-disposition", "attachment;filename=" + new Date().toString() + ".zip");

// 从list页面获取到用户多选的文件,value为存在map里的uuid
String[] uuids = request.getParameterValues("f");

if (uuids != null) {
// 拿到存储uuid:path的Map
Map<String, String> pathMap = (Map<String, String>) request.getSession().getAttribute("pathMap");
if (pathMap != null) {
System.out.println("zipping...");
// new一个java自带的zip处理流,非常方便
ZipOutputStream zoStream = new ZipOutputStream(response.getOutputStream());

for (String uuid : uuids) {
// 按uuid到path的Map里寻找文件对应的真实路径
String path = pathMap.get(uuid);
File file = new File(path);
FileInputStream fis = new FileInputStream(file);
// 关键:newZipEntry相当于新建一个压缩包内的文件
// putNextEntry相当于结束当前的entry,向zip流里写入一个新的entry
zoStream.putNextEntry(new ZipEntry(file.getName()));
// 使用IOUtils大大减少了我们的代码量
IOUtils.copy(fis, zoStream);
}
zoStream.flush();
zoStream.close();
}
}

System.out.println("ok!");
}

protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}

}

upload.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="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>
</head>
<body>
<form action="part" enctype="multipart/form-data" method="post">
<input name="usename">
<input type="file" name="file1">
<input type="file" name="file2">
<input type="submit" value="upload">
</form>
</body>
</html>

list.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!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>
</head>
<body>

<c:forEach items="${folderMap }" var="folder">
<a href="${pageContext.request.contextPath }/ListFilesServlet?uuid=${folder.value }">${folder.key }</a><br>
</c:forEach>
<form action="DownloadZipServlet" method="post">
<c:forEach items="${fileMap }" var="file">
<input type="checkbox" name="f" value="${file.value }"> <a href="${pageContext.request.contextPath }/DownloadOneServlet?fileId=${file.value }">${file.key }</a><br>
</c:forEach>
<input type="submit" value="download zip">

</form>
</body>
</html>