实现WEB 开发中的文件上传功能,需完成如下二步操作:
一、在WEB 页面中添加上传输入项,<input type=“life” name=“”> ,使用时注意:
1. 必须要设置input 输入项的name 属性,否则浏览器将不会发送上传文件的数据。
2. 必须把input 项的enctype 属值设为multipart/form-data. 设置该值后,浏览器
二、在上传文件时,将把文件数据附带在http 请求消息体中,并使用MIME协议对上传的文件进行描述,以方便接收方对上传数据进行解析和处理。
# 如何在Servlet中读取文件上传数据,并保存到服务器本地硬盘中?
Request 对象提供了一个getInputStream() 方法,使用这个方法可以获取浏览器提交过来的输入流。但如果浏览器上传多个文件时,我们应该如何区分开来?这是一项复杂的工作。
为了方便用户处理文件上传数据,Apache 开源组织提供了用于处理上面应用的开源组件——Commons-fileupload 。这个组件使用简单,性 能也比较优异,所以几乎都使用它来实现上传文件的处理功能。Struts 的上传文件处理部分也是使用它实现的。
#Fileupload 组件工作流程:
WEB 服务器
request
ServletFil
eupLoad
DiskFileItem
Factory
代表普通字段的
FileItem
代表上传文件1
FileItem
代表上传文件2
FileItem
isFileForm
getFieldName
getString
getInputStream
getName
getInputStream
getName
核心API-DiskFileItemFactory :
DiskFileItemFactory 是创建FileItem 对象的工厂,这个工厂常用方法:
1. public DiskFileItemFactory(int sizeThreshold, java.io.File repository) ,常用的构造函数。
2. public void setSizeThreshold(int sizeThreshold) ,设置内存缓冲区的大小,默认值为10K 。当上传文件大于缓冲区大小时, fileupload 组件将使用临时文件缓存上传文件。
3. public void setRepository(java.io.File repository) ,指定临时文件目录,默认值为System.getProperty("java.io.tmpdir") 。
核心API-ServletFileupLoad:
ServletFileUpload 负责处理上传的文件数据,并将表单中每个输入项封装到一个FileItem 对象中。常用方法有:
1. boolean isMultipartContent(HttpServletRequest request) ,判断上传表单是否为上传表单类型。
2. List parseRequest(HttpServletRequest request) ,解析request 对象,并把表单中的每一个输入项包装到一个fileItem 对象中,并返回一个保存了所有FileItem 的list 集合。
3. setFileSizeMax(long fileSizeMax) ,设置上传文件的最大尺寸值。
4. setSizeMax(long sizeMax) ,设置上传文件总量的最大值。
5. setHeaderEncoding(java.lang.String encoding) ,设置编码格式。如果文件路径中存在中文可能会造成文件路径乱码,用此方法处理可以解决。
6. setProgressListener(ProgressListener pListener) ,设置进程监听器,与AWT 和Swing 的事件处理机制一样。文件上传一点就会触发ProgressListener ,这样我们就可以获取文件上传的进度。
# 上传文件案例:
public class FileuploadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 创建文件处理工厂,它用于生成FileItem 对象。
DiskFileItemFactory difactory = new DiskFileItemFactory();
// 设置缓存大小,如果上传文件超过缓存大小,将使用临时目录做为缓存。
difactory.setSizeThreshold(1024 * 1024);
// 设置处理工厂缓存的临时目录,此目录下的文件需要手动删除。
String dir = this.getServletContext().getRealPath("/");
File filedir = new File(dir + "filetemp");
if (!filedir.exists())
filedir.mkdir();
difactory.setRepository(filedir);
// 设置文件实际保存的目录
String userdir = dir + "files";
File fudir = new File(userdir);
if (!fudir.exists())
fudir.mkdir();
// 创建request 的解析器,它会将数据封装到FileItem 对象中。
ServletFileUpload sfu = new ServletFileUpload(difactory);
// 解析保存在request 中的数据并返回list 集合
List list = null;
try {
list = sfu.parseRequest(request);
} catch (FileUploadException e) {
e.printStackTrace();
}
// 遍历list 集合,取出每一个输入项的FileItem 对象,并分别获取数据
for (Iterator it = list.iterator(); it.hasNext();) {
FileItem fi = (FileItem) it.next();
if (fi.isFormField()) {
System.out.println(fi.getFieldName());
System.out.println(fi.getString());
} else {
// 由于客户端向服务器发送的文件是客户端的全路径,在这我们只需要文件名即可
String filename = fi.getName();
int index = filename.lastIndexOf("\\");
if(index != -1)
filename = filename.substring(index+1);
// 向服务器写出文件
InputStream in = fi.getInputStream();
FileOutputStream fos = new FileOutputStream(fudir + "/" +filename);
byte[] buf = new byte[1024];
int len = -1;
while((len = in.read(buf)) != -1){
fos.write(buf, 0, len);
}
// 关闭流
if(in != null){
try{
in.close();
}finally{
if(fos!=null)
fos.close();
}
}
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
上面的代码只是功能的练习,实际开发中的文件上传需要考虑诸多因素,我们接下来继续学习。
#JS 动态添加文件上传框和按钮的JavaScript代码:
function add(){
var file = document.create_rElement_x("input");
file.type = "file";
file.name = "file";
var butt = document.create_rElement_x("input");
butt.type = "button";
butt.value = " 删除";
butt.onclick = function rem(){
// 必须使用按钮的父节点DIV 的父节点来删除自己和自己的父节点DIV 。
this.parentNode.parentNode.removeChild(this.parentNode);
};
var div = document.create_rElement_x("div");
div.a(file);
div.a(butt);
var parent = document.getElementByIdx_x_x("files");
parent.a(div);
}
#上传文件的处理细节(1):
1. 中文文件乱码的问题, 可以调用两个方法来设置字符编码:servletUpLoader.setHeaderEncoding() 或request.setCharacterEncoding() 。我们可以在源文件的创建ServletFileUpload 对象后边添加如下代码:
sfu.setHeaderEncoding("UTF-8");
2. 临时文件的删除,如果临时文件大于setSizeThreshold 设置的缓存大小,Commons-fileupload 组件将使用 setRepository 设置的临时目录来保存上传的文件,上传完成后我们需要手动调用FileItem.delete 来删除临时文件。建议不要修改缓 存区大小,如果设置缓存为1MB ,1000 个用户上传文件就需要1000MB 内存,服务器会受不了的。我们删除掉setSizeThreshold 的代 码,并在每次完成一个文件后添加下而的代码:
// 删除临时目录中的文件
fi.delete();
#上传文件的处理细节(2):
1. 在上面的代码中,我们将文件的实际保存目录设置在WEB-INF 目录之外。这样外部可以直接访问被上传的文件,这会造成安全问题。比如用户上传了一个带有 恶意脚本功能的JSP 文件,然后从外部访问执行了JSP 文件… 后果不堪设想。所以我们将源代码中对应位置处修改如下:
// 之所以放在"WEB-INF" 目录下是为了防止上传的文件被直接被访问的安全问题
String userdir = dir + "WEB-INF/files";
2. 一个WEB 应用会许多不同的用户访问,不同的用户可能会上传相同名称的文件,如果这样可能会造成文件覆盖的情况发生,所以我们必须保证文件名称的唯一性,我编写一个方法来生成唯一性名称的文件名:
private String uuidName(String fileName){
UUID uuid = UUID.randomUUID();
return uuid.toString() + "_" + fileName;
}
我们将代码“filename = filename.substring(index + 1);” 修改为:filename = uuidName(filename.substring(index + 1))
3. 如果一个目录下的文件过多,会极大减慢文件的访问速度。比如一个目录下的文件如果超过1000 个,达到1 万个呢?恐怖!我们必须编写一个目录结构生成算法,来分散存上传的文件。我们一个方法:
private String hashPath(String dir, String fileName) {
int hashCode = fileName.hashCode();
int dir1 = (hashCode >> 4) & 0xf;
int dir2 = hashCode & 0xf;
String newpath = dir + "/" + dir1 + "/" + dir2 + "/";
File file = new File(newpath);
if(!file.exists()){
file.mkdirs();
}
return newpath + uuidName(fileName);
}
上传文件的处理细节(3)
1. 使用ProgressListener 显示上传文件进度,在创建ServletFileUpload 之后添加如下代码:
// 设置文件上传进度监听器
sfu.setProgressListener(new ProgressListener() {
public void update(long pBytesRead, long pContentLength, int pItems) {
System.out.println(" 已上传:" + pBytesRead + " 总大小:"
+ pContentLength);
}
});
2. 上面的代码会造成频繁的打印,为了使它在上传一定数量后再打印,比如上传10KB 后再打印,我们修改上面的代码如下:
// 设置文件上传进度监听器
sfu.setProgressListener(new ProgressListener() {
long temp = -1;
public void update(long pBytesRead, long pContentLength, int pItems) {
long size = pBytesRead / 1024 * 1024 * 10;
if(temp == size)
return;
temp = size;
if(pBytesRead != -1)
System.out.println(" 已上传:" + pBytesRead + " 总大小:"
+ pContentLength);
else
System.out.println(" 上传完成!");
}
});
上面的代码比较经典,好好回味一下。
#文件下载:
WEB 应用中实现文件下载的两种方式:
1. 超链接直接指向下载资源
2. 程序实现下载需设置两个响应头:
(1). 设置Content-Type 的值为:application/x-msdownload 。Web 服务器需要告诉浏览器其所输出的内容的类型不是普通的文本文件或 HTML 文件,而是一个要保存到本地的下载文件。
(2). Web 服务器希望浏览器不直接处理相应的实体内容,而是由用户选择将相应的实体内容保存到一个文件中,这需要设置 Content-Disposition 报头。该报头指定了接收程序处理数据内容的方式,在 HTTP 应用中只有 attachment 是标准方式,attachment 表示要求用户干预。在 attachment 后面还可以指定 filename 参数,该参数是服务器建议浏览器将实体内容保存到文件中的文件名称。在设置 Content-Dispostion 之前一定要指定 Content-Type 。
为实现文件下载,首先我们遍历目录下所有文件,Servlet :
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
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 dir = this.getServletContext().getRealPath("/WEB-INF/files");
HashMap map = new HashMap();
listFile(new File(dir), map);
// 将文件列表设置到request 的属性中,然后由JSP 页面打印列表。
request.setAttribute("filemap", map);
request.getRequestDispatcher("/list.jsp").forward(request, response);
}
private void listFile(File f, HashMap map) {
if (f.isFile()) {
String path = f.getAbsolutePath().substring(
this.getServletContext().getRealPath("/").length());
String name = f.getName();
name = name.substring(name.indexOf("_")+1);
//BASE64Encoder encoder = new BASE64Encoder();
map.put(path, name);
} else {
File[] files = f.listFiles();
for (int i = 0; i < files.length; i++) {
listFile(files[i], map);
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}