一、文件上传概述
-
实现web开发中的文件上传功能,需完成如下二步操作
- 在web页面中添加上传输入项
- 在servlet中读取上传文件的数据,并保存到本地硬盘中。
-
如何在web页面中添加上传输入项
<input type="file">
标签用于在web页面中添加文件上传输入项,设置文件上传输入项时须注意:- 1、必须要设置input输入项的name属性,否则浏览器将不会发送上传文件的数据。
- 2、必须把form的enctype属性值设置为
multipart/form-data
。设置该值后,浏览器在上传文件时,将把文件数据附带在http请求消息体中,并使用MIME协议对上传文件进行描述,以方便接收方对上传数据进行解析和处理。
-
如何在Servlet中读取文件上传数据,并保存到本地硬盘中?
- Request对象提供了一个
getInputStream
方法,通过这个方法可以读取到客户端提交过来的数据。但由于用户可能会同时上传多个文件,在servlet端编程直接读取上传数据,并分别解析出相应的文件数据是一项非常麻烦的工作,示例。 - 为方便用户处理文件上传数据,Apache 开源组织提供了一个用来处理表单文件上传的一个开源组件(
Commons-fileupload
),该组件性能优异,并且其API使用极其简单,可以让开发人员轻松实现web文件上传功能,因此在web开发中实现文件上传功能,通常使用Commons-fileupload
组件实现,注意:此包依赖Commons-io
包。
- Request对象提供了一个
使用
Commons-fileupload
组件实现文件上传,需要导入该组件相应的支撑jar包:Commons-fileupload
和commons-io。commons-io
不属于文件上传组件的开发jar文件,但Commons-fileupload
组件从1.1 版本开始,它工作时需要commons-io
包的支持。
注意:其实tomcat已经继承了相应的工具包以完成上述两个工具包的功能,只是使用起来不太一样。这里我们还是使用上述两个包。fileupload组件工作流程
核心API-DiskFileItemFactory
DiskFileItemFactory
是创建 FileItem 对象的工厂,这个工厂类常用方法:
public void setSizeThreshold(int sizeThreshold)
设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。
public void setRepository(java.io.File repository)
指定临时文件目录,默认值为System.getProperty("java.io.tmpdir")
Public DiskFileItemFactory(int sizeThreshold, java.io.File repository)
构造函数-
核心API-ServletFileUpload
ServletFileUpload
负责处理上传的文件数据,并将表单中每个输入项封装成一个 FileItem 对象中。常用方法有:-
boolean isMultipartContent(HttpServletRequest request)
判断上传表单是否为multipart/form-data
类型 -
List parseRequest(HttpServletRequest request)
解析request对象,并把表单中的每一个输入项包装成一个fileItem 对象,并返回一个保存了所有FileItem的list集合。 -
setFileSizeMax(long fileSizeMax)
设置上传文件的最大值 -
setSizeMax(long sizeMax)
设置上传文件总量的最大值 -
setHeaderEncoding(java.lang.String encoding)
设置编码格式 -
setProgressListener(ProgressListener pListener)
设置监听器用来显示进度条
-
二、相关示例
2.1示例一
UploadServlet1.java
package cn.itcast.web.servlet;
//文件上传,这里我们还是用的commons-fileupload和commons-io,但是tomcat中已经包含了相关的文件上传的包
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
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.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.RequestContext;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
*/
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 UploadServlet1 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String savaPath = this.getServletContext().getRealPath("/WEB-INF/upload");//得到文件的保存目录,而这个目录一般不允许被人随意访问,所以应该放在WEB-INF下
//request.setCharacterEncoding("UTF-8");//这种方式不能解决提交的数据的乱码问题.表单为文件上传设置Request编码无效,只能手工设置
try{
DiskFileItemFactory factory = new DiskFileItemFactory();
//缓冲区默认是10k,下面我们设置如果上传文件超过10k则使用一个临时文件保存
factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setProgressListener(//这里我们定义一个监听器,可以看到文件上传的进度,之后可以使用ajax生成一个进度条,同时注意定义的位置,这里是在解析器定义之后解析完文件之前
new ProgressListener() {
@Override
public void update(long pBytesRead, long pContentLength, int arg2) {
System.out.println("文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead);
}
});
upload.setHeaderEncoding("UTF-8");//解决上传文件名在中文乱码问题
if(!upload.isMultipartContent(request)){//如果不是文件上传
//按照传统方式获取数据
return ;
}
/*upload.setFileSizeMax(1024);//上传的文件不能超过1k
upload.setSizeMax(1024*10);//上传文件的总大小不能超过10k
*/ List<FileItem> list = upload.parseRequest(request);
for(FileItem item : list){
if(item.isFormField()){
//fileItem中封装的是普通输入项的数据
String name = item.getFieldName();
//String value = item.getString();
//value = new String(value.getBytes("iso8859-1"),"UTF-8");//由于对Request设置编码无效,这里我们只能手工设置
String value = item.getString("UTF-8");//也可以用这种方式制定码表
System.out.println(name + "=" + value);
}else{//提交的表单是上传文件,不是普通输入项
//request.getParameter("username");//这种方式是获取不到数据的,因为表单提交时以multipart/form-data形式提交的
String filename = item.getName();//注意:不同浏览器提交文件的方式是不一样的,有的是完整路径,但是有的是直接提交1.txt,所以这里我们需要作相应的截取工作
if(filename == null || filename.trim().equals("")){
continue;//如果某个框中没有选择上传的文件则跳出本次循环,继续下一次循环
}
filename = filename.substring(filename.lastIndexOf("\\") + 1);
InputStream in = item.getInputStream();
String saveFileName = makeFileName(filename);//得到文件保存的名称
String realSavaPath = makePath(saveFileName, savaPath);//产生目录
FileOutputStream out = new FileOutputStream(realSavaPath + "\\" + saveFileName);
byte buffer[] = new byte[1024];
int len = 0;
while((len = in.read(buffer)) > 0){
out.write(buffer, 0, len);
}
in.close();
out.close();
item.delete();//删除临时文件,而且这行代码必须在上面流关闭的后面
}
}
}catch(FileUploadBase.FileSizeLimitExceededException e){
e.printStackTrace();
request.setAttribute("message", "文件超出最大值");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return ;
}catch(Exception e){
e.printStackTrace();
}
}
//产生唯一的文件名
public String makeFileName(String filename){
return UUID.randomUUID().toString() + "_" + filename;
}
//产生一个目录
public String makePath(String filename, String savePath){
int hashCode = filename.hashCode();//得到一个文件名的哈希编码,即文件在内存中的地址
int dir1 = hashCode & 0xf;//得到一个0-15的数,表示一级目录最多有16个目录
int dir2 = (hashCode & 0xf0) >> 4;
String dir = savePath + "\\" + dir1 + "\\" + dir2;//完整路径就是upload\2\3...
File file = new File(dir);
if(!file.exists()){
file.mkdirs();//因为这里会有多级目录,则用此方法。若是只有一级目录,那使用mkdir方法
}
return dir;
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
上传页面:upload.jsp
<body>
<form action="${pageContext.request.contextPath }/servlet/UploadServlet1" 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>
说明:
- 1.文件上传的整体过程就是首先我们得到一个
DiskFileItemFactory
工厂,然后设置好临时保存文件。 - 2.使用工厂得到
ServletFileUpload
解析器,解析器设置好监听器和文件名编码之后就可以进行文件解析了。 - 3.文件解析首先判断是不是文件上传(根据请求头判断),如果不是,则直接返回;如果是,再次判断是普通输入项数据还是文件数据,是普通输入项数据则按普通方式进行解析;是文件数据则进行文件解析。
注意:
-
1.上传文件名的中文乱码和上传数据的中文乱码问题
- 1.1对于上传的文件名的中文乱码问题我们可以使用
upload.setHeaderEncoding("UTF-8");
解决 - 1.2对于上传的数据的中文乱码问题我们可以使用
value = new String(value.getBytes("iso8859-1"),"UTF-8");
直接手工转换
String value = item.getString("UTF-8");
在获得数据的时候制定码表
- 1.1对于上传的文件名的中文乱码问题我们可以使用
2.为保证服务器安全,上传文件应该放在外界无法直接访问的目录,比如
WEB-INF
目录下。3.对于多次上传中出现相同的文件名问题我们需要解决:
String saveFileName = makeFileName(filename);
//得到文件保存的名称,使用UUID产生一个唯一的文件名
FileOutputStream out = new FileOutputStream(savaPath + "\\" + saveFileName);
4.为防止一个目录下出现太多文件,要使用hash算法打散存储
5.要限制上传文件大小的最大值,可以通过
upload.setFileSizeMax(1024);
//上传的文件不能超过1k。实现并通过捕获FileUploadBase.FileSizeLimitExceededException
异常给用户一个友好提示6.我们还可以设置当上传文件超出限制时(默认上传文件总大小是10k),使用临时文件保存
factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));
但是一般来说,文件会先保存在临时文件中,之后才会从临时文件复制到真正的存储目录中,而且这个临时文件一般不会删除,这里我们需要设置让其删除,如下。
7.要想确保临时文件被删除一定要在处理上传文件后关闭流之后调用
item.delete
方法。8.限制上传文件的类型
在收到上传文件名之后判断后缀名是否合法9.监听文件上传进度
upload.setProgressListener
10.当两个上传框中只有一个选择了上传文件而直接点击了提交:
if(filename == null || filename.trim().equals("")){
continue;//如果某个框中没有选择上传的文件则跳出本次循环,继续下一次循环
}
- 11.在web页面中动态添加上传输入项
upload2.jsp
有时候我们希望在上传文件的时候可以自己添加或删除上传文件的个数,这里可以对jsp进行修改:
<body>
<!-- 如果想提交则在下面套一个form表单 ,同时如果是上传文件的话一定要注意其enctype="mutlipart/form-data"-->
<!-- <form action="" enctype="mutlipart/form-data"> -->
<table>
<tr>
<td>上传用户:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>上传文件:</td>
<td><input type="button" value="添加上传文件" onclick="addInput()"></td>
</tr>
<tr>
<td> </td>
<td>
<div id="file">
</div>
</td>
</tr>
</table>
<script type="text/javascript">
function addInput(){
var div = document.getElementById("file");
var input = document.createElement("input");
input.type = "file";
input.name = "filename";
var del = document.createElement("input");
del.type = "button";
del.value = "删除";
del.onclick = function d(){
this.parentNode.parentNode.removeChild(this.parentNode);
}
var innerdiv = document.createElement("div");
innerdiv.appendChild(input);
innerdiv.appendChild(del);
div.appendChild(innerdiv);
}
</script>
</body>
三、文件下载
3.1 概述
-
web应用中实现文件下载的两种方式:
- 1.超链接直接指向下载的资源;
- 2.程序实现下载需要设置两个响应头:
<1>设置Content-Type
的值为:application/x-msdownload
。Web 服务器需要告诉浏览器其所输出的内容的类型不是普通的文本文件或 HTML 文件,而是一个要保存到本地的下载文件。
<2>Web 服务器希望浏览器不直接处理相应的实体内容,而是由用户选择将
相应的实体内容保存到一个文件中,这需要设置Content-Disposition
报头。该报头指定了接收程序处理数据内容的方式,在 HTTP 应用中只有attachment
是标准方式,attachment
表示要求用户干预。在attachment
后面还可以指定 filename 参数,该参数是服务器建议浏览器将实体内容保存到文件中的文件名称。在设置Content-Dispostion
之前一定要指定Content-Type
.
因为要下载的文件可以是各种类型的文件,所以要将文件传送给客户端,其相应的内容应该被当作二进制来处理,所以应该调用
response.getOutputStream
方法返回ServletOutputStream
对象来向客户端写入文件内容。
3.2 示例
列出网站所有文件:ListFileServlet.java
package cn.itcast.web.servlet;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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 filepath = this.getServletContext().getRealPath("/WEB-INF/upload");
Map map = new HashMap();
listFile(new File(filepath), map);
request.setAttribute("map", map);
request.getRequestDispatcher("/listfile.jsp").forward(request, response);
}
public void listFile(File file, Map map){
//为了让迭代出来的文件进行显示,这里我们一般传递一个map进行保存
if(!file.isFile()){//如果是目录(不是文件)
File files[] = file.listFiles();//得到所有文件或目录
for(File f : files){
listFile(f, map);
}
}else{//将迭代出的文件在jsp页面中进行显示
String realname = file.getName().substring(file.getName().indexOf("_") + 1);
map.put(file.getName(), realname);//强文件在硬盘中的名称作为key,而真实名称作为value
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
下载界面:listfile.jsp
<c:forEach var="me" items="${map }"><%-- filename=${me.key} 这里不能这样做,因为key中包含了中文--%>
<c:url value="/servlet/DownLoadServlet" var="downurl">
<c:param name="filename" value="${me.key}"></c:param>
</c:url>
${me.value } <a href="${downurl} ">下载</a><br/>
</c:forEach>
DownLoadServlet.java
package cn.itcast.web.servlet;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
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");
//filename = new String(filename.getBytes("iso8859-1"),"UTF-8");???我这里不需要设置编码
String path = makePath(filename, this.getServletContext().getRealPath("/WEB-INF/upload"));
File file = new File(path + "\\" + filename);
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"));
//这里注意:URLEncoder.encode对IE是有效的,但是对火狐无效
FileInputStream in = new FileInputStream(path + "\\" + filename);
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();
}
//得到要下载文件的目录
public String makePath(String filename, String savePath){
int hashCode = filename.hashCode();//得到一个文件名的哈希编码,即文件在内存中的地址
int dir1 = hashCode & 0xf;//得到一个0-15的数,表示一级目录最多有16个目录
int dir2 = (hashCode & 0xf0) >> 4;
String dir = savePath + "\\" + dir1 + "\\" + dir2;//完整路径就是upload\2\3...
File file = new File(dir);
if(!file.exists()){
file.mkdirs();//因为这里会有多级目录,则用此方法。若是只有一级目录,那使用mkdir方法
}
return dir;
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}