一、文件上传
将信息从个人计算机(本地计算机)传送至中央计算机(远程计算机)系统上,让网络上的人都能看到。
1.文件上传对页面的要求
(1)必须使用表单,而不能是超链接;
(2)表单的method必须是POST,而不能是GET;
(3)表单的enctype必须是multipart/form-data;
(4)在表单中添加file表单字段,即<input type=”file”…/>
<form action="${pageContext.request.contextPath }/FileUploadServlet" method="post" enctype="multipart/form-data">
用户名:<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>
2.文件上传对Servlet的要求
当提交的表单是文件上传表单时,那么对Servlet也是有要求的。
首先我们要肯定一点,文件上传表单的数据也是被封装到request对象中的。
request.getParameter(String)方法获取指定的表单字段字符内容,但文件上传表单已经不在是字符内容,而是字节内容,所以失效。
这时可以使用request的getInputStream()方法获取ServletInputStream对象,它是InputStream的子类,这个ServletInputStream对象对应整个表单的正文部分(从第一个分隔线开始,到最后),这说明我们需要的解析流中的数据。当然解析它是很麻烦的一件事情,而Apache已经帮我们提供了解析它的工具:commons-fileupload。
二、commons-fileupload
1.fieupload概述
fileupload是由apache的commons组件提供的上传组件。它最主要的工作就是帮我们解析request.getInputStream()。
fileupload组件需要的JAR包有:
commons-fileupload.jar,核心包;
commons-io.jar,依赖包。
2.fieupload的简单应用
fileupload的核心类有:DiskFileItemFactory、ServletFileUpload、FileItem。
使用fileupload组件的步骤如下:
1.创建工厂类DiskFileItemFactory对象:DiskFileItemFactory factory = new DiskFileItemFactory()
2.使用工厂创建解析器对象:ServletFileUpload fileUpload = new ServletFileUpload(factory)
3.使用解析器来解析request对象:List<FileItem> list = fileUpload.parseRequest(request)
一个FileItem对象对应一个表单项(表单字段)。一个表单中存在文件字段和普通字段,可以使用FileItem类的isFormField()方法来判断表单字段是否为普通字段,如果不是普通字段,那么就是文件字段了。
String getName():获取文件字段的文件名称;
String getString():获取字段的内容,如果是文件字段,那么获取的是文件内容,当然上传的文件必须是文本文件;
String getFieldName():获取字段名称,例如:<input type=”text” name=”username”/>,返回的是username;
String getContentType():获取上传的文件的类型,例如:text/plain。
int getSize():获取上传文件的大小;
boolean isFormField():判断当前表单字段是否为普通文本字段,如果返回false,说明是文件字段;
InputStream getInputStream():获取上传文件对应的输入流;
void write(File):把上传的文件保存到指定文件中。
文件上传代码简单实现:
shangchuan.jsp
<h1>上传</h1>
<form action="<c:url value='/Upload1Servlet'/>" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"/><br/>
选择图片:<input type="file" name="image"/><br/>
<input type="submit" value="上传"/><br/>
</form>
Upload1Servlet.java
package cn.yfy.fileload;
import java.io.File;
import java.io.IOException;
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;
import cn.itcast.commons.CommonUtils;
public class Upload1Servlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
// 创建工厂
DiskFileItemFactory factory = new DiskFileItemFactory();
// 得到解析器
ServletFileUpload sfu = new ServletFileUpload(factory);
// 解析request得到FileItem集合
try {
List<FileItem> fileItemList = sfu.parseRequest(request);
FileItem fi1 = fileItemList.get(0);
FileItem fi2 = fileItemList.get(1);
// 得到上传文件的表单项名字和值
System.out.println("表单项名称为" + fi1.getFieldName());
System.out.println("表单项的值为" + fi1.getString());
// 得到上传文件的字节数、大小和文件类型
System.out.println("长传的字节数" + fi2.getSize());
System.out.println("上传的文件名称为" + fi2.getName());
System.out.println("上传的文件类型为" + fi2.getContentType());
// 得到文件保存的路径
String root = this.getServletContext()
.getRealPath("/WEB-INF/files/");
// 得到用户上传的文件名,处理可能出现的绝对路径问题
System.out.println("root:"+root);
String filename = fi2.getName();
int index = filename.lastIndexOf("\\");
if (index != -1) {
filename = filename.substring(index + 1);
}
System.out.println("filename"+filename);
// 得到hashCode
int hCode = filename.hashCode();
// 转换成16进制
String hex = Integer.toHexString(hCode);
// 获取前两个字符生成二级目录
File dirFile = new File(root, hex.charAt(0) + "/" + hex.charAt(1));
// 创建目录
dirFile.mkdirs();
// 得到保存时的文件名,加上uuid确保不会重名
String savename = CommonUtils.uuid() + filename;
System.out.println(savename);
File destFile = new File(dirFile, savename);
try {
fi2.write(destFile);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileUploadException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
三、文件上传之细节
1.把上传的文件放到WEB-INF目录下
如果没有把用户上传的文件存放到WEB-INF目录下,那么用户就可以通过浏览器直接访问上传的文件,这是非常危险的。
假如说用户上传了一个a.jsp文件,然后用户在通过浏览器去访问这个a.jsp文件,那么就会执行a.jsp中的内容,如果在a.jsp中有如下语句:Runtime.getRuntime().exec(“shutdown –s –t 1”);,那么你就会…
通常我们会在WEB-INF目录下创建一个uploads目录来存放上传的文件,而在Servlet中找到这个目录需要使用ServletContext的getRealPath(String)方法,例如在我的upload1项目中有如下语句:
ServletContext servletContext = this.getServletContext();
String savepath = servletContext.getRealPath(“/WEB-INF/uploads”);
其中savepath为:F:\tomcat6_1\webapps\upload1\WEB-INF\uploads。
2.文件名称(完整路径)
使用不同浏览器测试,其中IE6就会返回上传文件的完整路径,这给我们带来了很大的麻烦,就是需要处理这一问题。
处理这一问题也很简单,无论是否为完整路径,我们都去截取最后一个“\\”后面的内容就可以了。
String name = file1FileItem.getName();
int lastIndex = name.lastIndexOf("\\");//获取最后一个“\”的位置
if(lastIndex != -1) {//注意,如果不是完整路径,那么就不会有“\”的存在。
name = name.substring(lastIndex + 1);//获取文件名称
}
response.getWriter().print(name);
3.中文乱码问题
上传文件名称中包含中文:
当上传的谁的名称中包含中文时,需要设置编码,commons-fileupload组件为我们提供了两种设置编码的方式:
request.setCharacterEncoding(String):这种方式是我们最为熟悉的方式了;
fileUpload.setHeaderEncdoing(String):这种方式的优先级高与前一种。
上传文件的文件内容包含中文:
通常我们不需关心上传文件的内容,因为我们会把上传文件保存到硬盘上!也就是说,文件原来是什么样子,到服务器这边还是什么样子!
但是如果你要在控制台显示上传的文件内容,那么你可以使用fileItem.getString(“utf-8”)来处理编码。
文本文件内容和普通表单项内容使用FileItem类的getString(“utf-8”)来处理编码。
4.上传文件同名问题
通常我们会把用户上传的文件保存到uploads目录下,但如果用户上传了同名文件呢?这会出现覆盖的现象。处理这一问题的手段是使用UUID生成唯一名称,然后再使用“_”连接文件上传的原始名称。
例如用户上传的文件名称是“a.jpg”,在uuid自动生成随机数处理后,文件名称为:“DC15EF727F144376864E99450A82268Da.jpg”,这种手段不会使文件丢失扩展名,并且因为UUID的唯一性,上传的文件同名,但在服务器端是不会出现同名问题的。
5.一个目录不能存放过多的文件
我们这里使用hash算法来打散:
1.获取文件名称的hashCode:int hCode = name.hashCode();;
2.获取hCode的低4位,然后转换成16进制字符;
3.获取hCode的5~8位,然后转换成16进制字符;
4.使用这两个16进制的字符生成目录链。例如低4位字符为“5”
这种算法的好处是,在uploads目录下最多生成16个目录,而每个目录下最多再生成16个目录,即256个目录,所有上传的文件都放到这256个目录下。如果每个目录上限为1000个文件,那么一共可以保存256000个文件。
6.上传文件的大小限制
有时我们需要限制一个请求的大小。也就是说这个请求的最大字节数(所有表单项之和)!实现这一功能也很简单,只需要调用ServletFileUpload类的setSizeMax(long)方法即可。还可以限制文件大小,只需要调用调用ServletFileUpload类的setFileSizeMax(long)方法即可。上面的两个方法必须在解析之前开始调用。
例如fileUpload.setSizeMax(1024 * 10);,显示整个请求的上限为10KB。当请求大小超出10KB时,ServletFileUpload类的parseRequest()方法会抛出FileUploadBase.SizeLimitExceededException异常。
例如fileUpload.setFileSizeMax(1024 * 10);,显示文件的上限为10KB。当文件大小超出10KB时,ServletFileUpload类的parseRequest()方法会抛出FileUploadBase.FileSizeLimitExceededException异常。
7.缓存大小与临时目录
大家想一想,如果我上传一个蓝光电影,先把电影保存到内存中,然后再通过内存copy到服务器硬盘上,那么你的内存能吃的消么?
所以fileupload组件不可能把文件都保存在内存中,fileupload会判断文件大小是否超出10KB,如果是那么就把文件保存到硬盘上,如果没有超出,那么就保存在内存中。
10KB是fileupload默认的值,我们可以来设置它。
当文件保存到硬盘时,fileupload是把文件保存到系统临时目录,当然你也可以去设置临时目录。
DiskFileItemFactory dfif = new DiskFileItemFactory(1024*20, new File("F:\\temp"));
指定缓存大小为20KB,当上传的文件超出了20KB就会保存到临时目录。临时目录为F:\\temp
四、文件下载
1.通过Servlet下载
被下载的资源必须放到WEB-INF目录下(只要用户不能通过浏览器直接访问就OK),然后通过Servlet完成下载。
在jsp页面中给出超链接,链接到DownloadServlet,并提供要下载的文件名称。然后DownloadServlet获取文件的真实路径,然后把文件写入到response.getOutputStream()流中。
还需要设置两个响应头。
> Content-Type:你传递给客户端的文件是什么MIME类型,例如:image/pjpeg
* 通过文件名称调用ServletContext的getMimeType()方法,得到MIME类型!
> Content-Disposition:它的默认值为inline,表示在浏览器窗口中打开!attachment;filename=xx
2.下载框中文乱码问题
(1)给设置响应头的filename进行如下编码
filename = new String(filename.getBytes("GBK"), "ISO-8859-1");
response.addHeader("content-disposition", "attachment;filename=" + filename);
(2)调用下面的方法
package cn.yfy.download;
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.http.HttpServletRequest;
import sun.misc.BASE64Encoder;
public class FilenameEncoding {
public static String filenameEncoding(String filename, HttpServletRequest request) throws IOException {
String agent = request.getHeader("User-Agent"); //获取浏览器
if (agent.contains("Firefox")) {
BASE64Encoder base64Encoder = new BASE64Encoder();
filename = "=?utf-8?B?"
+ base64Encoder.encode(filename.getBytes("utf-8"))
+ "?=";
} else if(agent.contains("MSIE")) {
filename = URLEncoder.encode(filename, "utf-8");
} else {
filename = URLEncoder.encode(filename, "utf-8");
}
return filename;
}
}
3.代码演示
package cn.yfy.download;
import java.io.FileInputStream;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
public class DownLoadServlet extends HttpServlet {
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
/*
* 两个头一个流 1. Content-Type 2. Content-Disposition 3. 流:下载文件的数据
*/
String filename = "F:/追梦赤子心.mp3";
// 为了使下载框中显示中文文件名称不出乱码!
// String framename = new String("追梦赤子心.mp3".getBytes("GBK"),
// "ISO-8859-1");
String framename = FilenameEncoding.filenameEncoding("追梦赤子心.mp3", req);
// 通过文件名称获取MIME类型
String contentType = this.getServletContext().getMimeType(filename);
String contentDisposition = "attachment;filename=" + framename;
// 一个流
FileInputStream input = new FileInputStream(filename);
// 设置头
resp.setHeader("Content-Type", contentType);
resp.setHeader("Content-Disposition", contentDisposition);
// 获取绑定了响应端的流
ServletOutputStream output = resp.getOutputStream();
// 把输入流中的数据写入到输出流中。
IOUtils.copy(input, output);
// 关闭流
input.close();
}
}