文章转载自:http://www.cnblogs.com/xdp-gacl/p/4200090.html
看过这篇文章之后深有感触,并对文章中不足的地方加以改善。
在Web应用系统开发中,文件上传和下载功能是非常常用的功能,今天来讲一下JavaWeb中的文件上传和下载功能的实现。
对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用Servlet获取上传文件的输入流然后再解析里面的请求参数是比较麻烦,所以一般选择采用apache的开源工具common-fileupload这个文件上传组件。这个common-fileupload上传组件的jar包可以去apache官网上面下载,也可以在struts的lib文件夹下面找到,struts上传的功能就是基于这个实现的。common-fileupload是依赖于common-io这个包的,所以还需要下载这个包。
一、开发环境搭建
创建项目UploadTest,加入Apache的commons-fileupload文件上传组件的相关Jar包,如下图所示:
二、实现文件上传
2.1、文件上传页面和消息提示页面
upload.jsp页面的代码如下:
<%@ page language="java" import="java.util.*" contentType="text/html; charset=utf-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>文件上传</title>
</head>
<body>
<form action="servlet/UploadHandleServlet" enctype="multipart/form-data" method="post">
上传用户:<input type="text" name="username"><br>
上传文件1:<input type="file" name="file1"><br>
上传文件2:<input type="file" name="file2"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
很简单的界面,可以上传文字或者是文件,效果图如下:
message.jsp负责显示上传的结果,代码如下:
<%@ page language="java" import="java.util.*" contentType="text/html; charset=utf-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>消息提示</title>
</head>
<body>
${message}
</body>
</html>
2.2、处理文件上传的Servlet
UploadHandleServlet的代码如下:
package servlet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
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.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
public class UploadHandleServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 得到上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
String savePath = this.getServletContext().getRealPath(
"/WEB-INF/upload");
File file = new File(savePath);
// 判断上传文件的保存目录是否存在
if (!file.exists() && !file.isDirectory()) {
// 创建目录
file.mkdir();
}
// 上传消息提示
String message = "";
// 使用Apache文件上传组件处理文件上传步骤:
// 1、创建一个DiskFileItemFactory工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 2、创建一个文件上传解析器
ServletFileUpload upload = new ServletFileUpload(factory);
// 解决上传文件名的中文乱码
upload.setHeaderEncoding("utf-8");
// 3、判断提交上来的数据是否是上传表单的数据
if (!ServletFileUpload.isMultipartContent(request)) {
// 不是multipart/form-data方式提交
return;
}
// 4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
try {
List<FileItem> list = upload.parseRequest(request);
for (FileItem item : list) {
if (item.isFormField()) {
// 普通数据项
String name = item.getFieldName();
String value = item.getString("utf-8");
} else {
// 文件数据
// 文件名称
String fileName = item.getName();
if (fileName == null || "".equals(fileName.trim())) {
// 文件名为空,不保存数据
continue;
}
// 注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,
// 如: c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
// 只保留文件名,并且区分不同系统的分隔符
System.out.println("上传的文件名为:" + fileName);
fileName = fileName.substring(fileName
.lastIndexOf(File.separator) + 1);
System.out.println("保存的文件名为:" + fileName);
// 获取item中的上传文件的输入流,以及文件输出流
InputStream is = item.getInputStream();
OutputStream os = new FileOutputStream(savePath + "\\"
+ fileName);
// 以下是文件的保存
// 创建缓冲区
byte[] buffer = new byte[1024 * 5];
int len = 0;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
is.close();
os.close();
// 删除处理文件上传时生成的临时文件
item.delete();
message = "上传成功";
}
}
} catch (FileUploadException e) {
message = "上传失败,请重试";
e.printStackTrace();
}
request.setAttribute("message", message);
request.getRequestDispatcher("/message.jsp").forward(request, response);
}
}
在Web.xml文件中注册UploadHandleServlet
<servlet>
<servlet-name>UploadHandleServlet</servlet-name>
<servlet-class>servlet.UploadHandleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadHandleServlet</servlet-name>
<url-pattern>/servlet/UploadHandleServlet</url-pattern>
</servlet-mapping>
好了,此时就可以上传文件了,有兴趣的可以实验一下,中文名的文件也可以正常上传了。
2.3、文件上传的细节
上述的代码虽然可以成功将文件上传到服务器上面的指定目录当中,但是文件上传功能有许多需要注意的小细节问题,以下列出的几点需要特别注意的:
1、为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如放于WEB-INF目录下。
2、为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名。
3、为防止一个目录下面出现太多文件,要使用hash算法打散存储。
4、要限制上传文件的最大值。
5、要限制上传文件的类型,在收到上传文件名时,判断后缀名是否合法。
针对上述提出的5点细节问题,我们来改进一下UploadHandleServlet,改进后的代码如下:
package servlet;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.UUID;
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.FileUploadBase;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
public class UploadHandleServlet extends HttpServlet {
private long fileSize;
private long totalSize;
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 得到上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
String savePath = this.getServletContext().getRealPath(
"/WEB-INF/upload");
File saveFile = new File(savePath);
// 上传时生成的临时文件保存目录
String tempPath = this.getServletContext().getRealPath("/WEB-INF/temp");
File tempFile = new File(tempPath);
// 判断上传文件的保存目录是否存在
if (!tempFile.exists()) {
// 创建目录
tempFile.mkdir();
}
// 上传消息提示
String message = "";
try {
// 使用Apache文件上传组件处理文件上传步骤:
// 1、创建一个DiskFileItemFactory工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 设置工厂的缓冲区的大小,当上传的文件大小超过缓冲区的大小时,就会生成一个临时文件存放到指定的临时目录当中。
factory.setSizeThreshold(1024 * 100);// 设置缓冲区的大小为100KB,如果不指定,那么缓冲区的大小默认是10KB
// 设置上传时生成的临时文件的保存目录
factory.setRepository(tempFile);
// 2、创建一个文件上传解析器
ServletFileUpload upload = new ServletFileUpload(factory);
// 监听文件上传进度
upload.setProgressListener(new ProgressListener() {
public void update(long arg0, long arg1, int arg2) {
System.out.println("文件大小为:" + arg1 + ",当前已处理:" + arg0);
}
});
// 解决上传文件名的中文乱码
upload.setHeaderEncoding("utf-8");
// 3、判断提交上来的数据是否是上传表单的数据
if (!ServletFileUpload.isMultipartContent(request)) {
// 不是multipart/form-data方式提交
return;
}
// 设置上传单个文件的大小的最大值,目前是设置为1024*1024*100字节,也就是100MB
upload.setFileSizeMax(1024 * 1024 * 100);
// 设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,目前设置为200MB
upload.setSizeMax(1024 * 1024 * 100);
// 4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
List<FileItem> list = upload.parseRequest(request);
for (FileItem item : list) {
if (item.isFormField()) {
// 普通数据项
String name = item.getFieldName();
String value = item.getString("utf-8");
} else {
// 文件数据
// 文件名称
String fileName = item.getName();
if (fileName == null || "".equals(fileName.trim())) {
// 文件名为空,不保存数据
continue;
}
// 注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,
// 如: c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt
// 只保留文件名,并且区分不同系统的分隔符
System.out.println("上传的文件名为:" + fileName);
fileName = fileName.substring(fileName
.lastIndexOf(File.separator) + 1);
System.out.println("保存的文件名为:" + fileName);
// 得到上传文件的扩展名
String fileExtName = fileName.substring(fileName
.lastIndexOf(".") + 1);
// 如果需要限制上传的文件类型,那么可以通过文件的扩展名来判断上传的文件类型是否合法
System.out.println("上传的文件的扩展名是:" + fileExtName);
// 获取item中的上传文件的输入流,以及文件输出流
InputStream is = item.getInputStream();
// 得到文件保存的名称
String saveFilename = makeFileName(fileName);
// 得到文件的保存目录
String realSavePath = makePath(saveFilename, savePath);
OutputStream os = new FileOutputStream(realSavePath + "\\"
+ fileName);
// 以下是文件的保存
// 创建缓冲区
byte[] buffer = new byte[1024 * 5];
int len = 0;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
is.close();
os.close();
// 删除处理文件上传时生成的临时文件
// item.delete();
message = "上传成功";
}
}
} catch (FileUploadBase.FileSizeLimitExceededException e) {
e.printStackTrace();
request.setAttribute("message", "单个文件超出最大值!!!");
request.getRequestDispatcher("/message.jsp").forward(request,
response);
return;
} catch (FileUploadBase.SizeLimitExceededException e) {
e.printStackTrace();
request.setAttribute("message", "上传文件的总的大小超出限制的最大值!!!");
request.getRequestDispatcher("/message.jsp").forward(request,
response);
return;
} catch (Exception e) {
message = "文件上传失败!";
e.printStackTrace();
}
request.setAttribute("message", message);
request.getRequestDispatcher("/message.jsp").forward(request, response);
}
/**
* @function 生成上传文件的文件名,文件名以:uuid+"_"+文件的原始名称
* @param fileName
* @return
*/
private String makeFileName(String fileName) {
// 为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名
return UUID.randomUUID().toString() + "_" + fileName;
}
/**
* 为防止一个目录下出现太多文件,使用hash算法打散存储
*
* @param filename
* @param savePath
* @return
*/
private String makePath(String filename, String savePath) {
// 得到文件名的hashCode的值,得到的就是filename这个字符串对象在内存中的地址
int hashcode = filename.hashCode();
int dir1 = hashcode & 0xf; // 0--15
int dir2 = (hashcode & 0xf0) >> 4; // 0-15
// 构造新的保存目录
String dir = savePath + "\\" + dir1 + "\\" + dir2; // upload\2\3
// upload\3\5
// File既可以代表文件也可以代表目录
File file = new File(dir);
// 如果目录不存在
if (!file.exists()) {
file.mkdirs();
}
return dir;
}
}
这里有一个问题,设置文件上传的最大值,当上传的文件远大于设定的值,虽然能捕获异常,但是无法正常跳转;当文件的大小比设定
的大小稍微大一些时,就能正常捕获异常,跳转也正常。
有知道解决方法的可以留言,谢谢。
三、文件下载
3.1、列出提供下载的文件资源
我们要将Web应用系统中的文件资源提供给用户进行下载,首先我们要有一个页面列出上传文件目录下的所有文件,当用户点击文件下载超链接时就进行下载操作,编写一个ListFileServlet,用于列出Web应用系统中所有下载文件。
ListFileServlet的代码如下:
package servlet;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ListFileServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取上传文件的目录
String uploadFilePath = this.getServletContext().getRealPath(
"/WEB-INF/upload");
// 存储要下载的文件名
Map<String, String> fileNameMap = new HashMap<String, String>();
// 递归遍历filepath目录下的所有文件和目录,将文件的文件名存储到map集合中
listfile(new File(uploadFilePath), fileNameMap);// File既可以代表一个文件也可以代表一个目录
// 将Map集合发送到listfile.jsp页面进行显示
request.setAttribute("fileNameMap", fileNameMap);
request.getRequestDispatcher("/listfile.jsp")
.forward(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
public void listfile(File file, Map<String, String> map) {
if (!file.isFile()) {
File files[] = file.listFiles();
for (File f : files) {
listfile(f, map);
}
} else {
String realName = file.getName().substring(
file.getName().indexOf("_") + 1);
System.out.println(file.getName() + " " + realName);
map.put(file.getName(), realName);
}
}
}
这里简单说一下ListFileServlet中listfile方法,listfile方法是用来列出目录下的所有文件的,listfile方法内部用到了递归,在实际开发当中,我们肯定会在数据库创建一张表,里面会存储上传的文件名以及文件的具体存放目录,我们通过查询表就可以知道文件的具体存放目录,是不需要用到递归操作的,这个例子是因为没有使用数据库存储上传的文件名和文件的具体存放位置,而上传文件的存放位置又使用了散列算法打散存放,所以需要用到递归,在递归时,将获取到的文件名存放到从外面传递到listfile方法里面的Map集合当中,这样就可以保证所有的文件都存放在同一个Map集合当中。
在Web.xml文件中配置ListFileServlet
1 <servlet>
2 <servlet-name>ListFileServlet</servlet-name>
3 <servlet-class>me.gacl.web.controller.ListFileServlet</servlet-class>
4 </servlet>
5
6 <servlet-mapping>
7 <servlet-name>ListFileServlet</servlet-name>
8 <url-pattern>/servlet/ListFileServlet</url-pattern>
9 </servlet-mapping>
展示下载文件的listfile.jsp页面如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>下载文件显示界面</title>
</head>
<body>
<!-- 遍历Map集合 -->
<c:forEach var="me" items="${fileNameMap}">
<c:url value="DownLoadServlet" var="downurl">
<c:param name="filename" value="${me.key}"></c:param>
</c:url>
${me.value}<a href="${downurl}">下载</a>
<br/>
</c:forEach>
</body>
</html>
访问ListFileServlet,就可以在listfile.jsp页面中显示提供给用户下载的文件资源,如下图所示:
3.2、实现文件下载
原文中文件下载是不能实现的,因为获取文件的路径方法存在问题,我在其上加以改进,亲测有效:
编写一个用于处理文件下载的Servlet,DownLoadServlet的代码如下:
package servlet;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class DownLoadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 得到要下载的文件名
String fileName = request.getParameter("filename"); // 23239283-92489-阿凡达.avi
// 上传的文件都是保存在/WEB-INF/upload目录下的子目录当中
String fileSaveRootPath = this.getServletContext().getRealPath(
"/WEB-INF/upload");
// 通过文件名找出文件的所在目录
ArrayList<String> list = new ArrayList<String>();
list = findFileSavePathByFileName(fileName, new File(fileSaveRootPath),
list);
String path = list.get(0);
// 得到要下载的文件
File file = new File(path);
System.out.println("下载的文件的路径是:" + path);
// 如果文件不存在
if (!file.exists()) {
request.setAttribute("message", "您要下载的资源已被删除!!");
request.getRequestDispatcher("/message.jsp").forward(request,
response);
return;
}
// 处理文件名
String realname = fileName.substring(fileName.indexOf("_") + 1);
// 设置响应头,控制浏览器下载该文件
response.setHeader("content-disposition", "attachment;filename="
+ URLEncoder.encode(realname, "UTF-8"));
// 读取要下载的文件,保存到文件输入流
FileInputStream in = new FileInputStream(path);
// 创建输出流
OutputStream out = response.getOutputStream();
// 创建缓冲区
byte buffer[] = new byte[1024];
int len = 0;
// 循环将输入流中的内容读取到缓冲区当中
while ((len = in.read(buffer)) > 0) {
// 输出缓冲区的内容到浏览器,实现文件下载
out.write(buffer, 0, len);
}
// 关闭文件输入流
in.close();
// 关闭输出流
out.close();
}
/**
* @Method: findFileSavePathByFileName
* @Description: 通过文件名和存储上传文件根目录找出要下载的文件的所在路径
* @param filename
* 要下载的文件名
* @param saveRootPath
* 上传文件保存的根目录,也就是/WEB-INF/upload目录
* @return 要下载的文件的存储目录
*/
public ArrayList<String> findFileSavePathByFileName(String filename,
File dir, ArrayList<String> list) {
File[] files = dir.listFiles();
File tempFile;
for (int i = 0; i < files.length; i++) {
tempFile = files[i];
if (tempFile.isDirectory()) {
findFileSavePathByFileName(filename, tempFile, list);
} else if (tempFile.isFile()) {
if (tempFile.getName().equals(filename)) {
list.add(tempFile.getAbsolutePath());
}
}
}
return list;
}
public void listfile(File file, Map<String, String> map) {
if (!file.isFile()) {
File files[] = file.listFiles();
for (File f : files) {
listfile(f, map);
}
} else {
String realName = file.getName().substring(
file.getName().indexOf("_") + 1);
System.out.println(file.getName() + " " + realName);
map.put(file.getName(), realName);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
}
所有的上传下载都已经实现了,有兴趣的朋友可以留言交流。