JavaWeb学习——文件上传下载

一、文件上传

1.1 文件上传对页面的要求
  1. 必须使用form表单,而不能是超链接;
  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>
1.2 文件上传表单和普通文本表单的区别
  • 文件上传表单:
    请求方式只能是post,且需要设置form表单的属性enctype=”multipart/form-data”,含义为:多部件表单数据;
  • 普通文本表单:
    可以不设enctype属性,有默认值
    当method=”post”时,enctype的默认值为application/x-www-form-urlencoded,表示使用url编码正文;
    当method=”get”时,enctype的默认值为null,get方式没有正文,所以就不需要enctype了。
1.3 多部件表单介绍
  1. 设置form表单的属性为enctype=”multipart/form-data”后,表单的请求正文部分则是由多个部件组成,每个部件对应一个表单字段,每个部件都有自己的头信息。头信息下面是空行,空行下面是字段的正文部分。多个部件之间使用随机生成的分隔线隔开。
  2. 多部件表单的特点:
    1). 分隔出多个部件,即一个表单项一个部件。
    2). 一个部件中自己包含请求头和空行,以及请求体。
    3). 普通表单项:
      请求头(1个):Content-Disposition:包含name=“xxxx”,即表单项名称。
      请求体:表单项的值
    4). 文件表单项:
      请求头(2个):
        Content-Disposition:包含name=“xxxx”,即表单项名称;还有一个filename=“xxx”,表示上传文件的名称
        Content-Type:它是上传文件的MIME类型,例如:image/pjpeg,表示上传的是图片,图上中jpg扩展名的图片。
      请求体:体就是上传文件的内容。
    JavaWeb学习——文件上传下载
1.4 文件上传对Servlet的要求
  • 不能再使用request.getParametere(“xxx”)获取表单数据;
    这个方法在表单为enctype="multipart/form-data"时,无法获取请求参数,永远都返回null,因为request.getParameter(String)方法获取指定的表单字段字符内容,但文件上传表单已经不在是字符内容,而是字节内容,所以失效。
  • 可以使用ServletInputStream request.getInputStream()获取所有的表单数据,它对应整个表单的正文部分。
1.5 commons-fileupload

  使用ServletInputStream request.getInputStream()获取整个表单的请求正文信息后,我们需要解析流中的数据。当然解析它是很麻烦的一件事情,而Apache已经帮我们提供了解析它的工具:commons-fileupload。

  1. fileupload概述:
    fileupload是由apache的commons组件提供的上传组件。它最主要的工作就是帮我们解析request.getInputStream(),解析后的结果是一个表单项数据封装到一个FileItem对象中。我们只需要调用FileItem的方法即可!

  2. fileupload使用前准备(两个jar包):
    commons-fileupload.jar:核心包;
    commons-io.jar:依赖包。

  3. fileupload的核心类有:
    工厂类:DiskFileItemFactory
    解析器类:ServletFileUpload
    表单项类:FileItem

  4. fileupload组件的使用步骤:
    1).创建工厂类对象:DiskFileItemFactory factory = new DiskFileItemFactory()
    2).使用工厂创建解析器对象:ServletFileUpload fileUpload = new ServletFileUpload(factory)
    3).使用解析器来解析request对象:List list = fileUpload.parseRequest(request)

  5. FileItem类介绍:
    一个FileItem对象对应一个表单项(表单字段)。一个表单中存在文件字段和普通字段,可以使用FileItem类的isFormField()方法来判断表单字段是否为普通字段,如果不是普通字段,那么就是文件字段了。

    下面列出FileItem类的常用方法:
       String getName():获取文件字段的文件名称;
       String getString():获取字段的内容,如果是文件字段,那么获取的是文件内容,当然上传的文件必须是文本文件;
       String getFieldName():获取字段名称,例如:,返回的是username;
       String getContentType():获取上传的文件的类型,例如:text/plain。
       int getSize():获取上传文件的大小;
       boolean isFormField():判断当前表单字段是否为普通文本字段,如果返回false,说明是文件字段;
       InputStream getInputStream():获取上传文件对应的输入流;
       void write(File):把上传的文件保存到指定文件中。

  6. 简单上传示例
    写一个简单的上传示例:
       表单包含一个用户名字段,以及一个文件字段;
       Servlet保存上传的文件到uploads目录,显示用户名,文件名,文件大小,文件类型。

第一步:
完成index.jsp,只需要一个表单。注意表单必须是post的,而且enctype必须是mulitpart/form-data的。

<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/>
    <input type="submit" value="提交"/>
</form>

第二步:
完成FileUploadServlet

public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
	// 因为要使用response打印,所以设置其编码
	response.setContentType("text/html;charset=utf-8");
	
	// 创建工厂
	DiskFileItemFactory dfif = new DiskFileItemFactory();
	// 使用工厂创建解析器对象
	ServletFileUpload fileUpload = new ServletFileUpload(dfif);
	try {
		// 使用解析器对象解析request,得到FileItem列表
		List<FileItem> list = fileUpload.parseRequest(request);
		// 遍历所有表单项
		for(FileItem fileItem : list) {
			// 如果当前表单项为普通表单项
			if(fileItem.isFormField()) {
				// 获取当前表单项的字段名称
				String fieldName = fileItem.getFieldName();
				// 如果当前表单项的字段名为username
				if(fieldName.equals("username")) {
					// 打印当前表单项的内容,即用户在username表单项中输入的内容
					response.getWriter().print("用户名:" + fileItem.getString() + "<br/>");
				}
			} else {//如果当前表单项不是普通表单项,说明就是文件字段
				String name = fileItem.getName();//获取上传文件的名称
				// 如果上传的文件名称为空,即没有指定上传文件
				if(name == null || name.isEmpty()) {
					continue;
				}
				// 获取真实路径,对应${项目目录}/uploads,当然,这个目录必须存在
				String savepath = this.getServletContext().getRealPath("/uploads");
				// 通过uploads目录和文件名称来创建File对象
				File file = new File(savepath, name);
				// 把上传文件保存到指定位置
				fileItem.write(file);
				// 打印上传文件的名称
				response.getWriter().print("上传文件名:" + name + "<br/>");
				// 打印上传文件的大小
				response.getWriter().print("上传文件大小:" + fileItem.getSize() + "<br/>");
				// 打印上传文件的类型
				response.getWriter().print("上传文件类型:" + fileItem.getContentType() + "<br/>");
			}
		}
	} catch (Exception e) {
		throw new ServletException(e);
	} 
}

1.6 文件上传的细节
  1. 将文件保存到WEB-INF下:
    可以防止浏览器直接访问到资源文件。
  2. 文件名称相关问题:
    有的浏览器上传的文件名是绝对路径,如:C:\files\baibing.jpg,这时需要切割。
String filename = fi2.getName();
int index = filename.lastIndexOf("\\");
if(index != -1) {
    filename = filename.substring(index+1);
}
  1. 文件名乱码或者普通表单项乱码问题:
    可通过设置:request.setCharacterEncoding(“utf-8”)或调用servletFileUpload.setHeaderEncoding(“utf-8”)解决上传文件名乱码问题。

    注:fileupload的内部会调用request.getCharacterEncoding();

  2. 文件同名问题:
    解决方案:为每个文件添加名称前缀,这个前缀要保证不能重复。
    例如:filename = CommonUtils.uuid() + “_” + filename;

  3. 目录打散:
    不能在一个目录下存放太多文件,太多文件会导致文件检索及操作变慢。

    一般有以下几种文件目录打散方式:
    1).首字符打散:使用文件的首字母做为目录名称,例如:abc.txt,那么我们把文件保存到a目录下。如果a目录这时不存在,那么创建之。
    2).时间打散:使用当前日期做为目录。
    3).哈希打散:
      a.通过文件名称得到int值,即调用hashCode()
      b.它int值转换成16进制0~9, A~F
      c.获取16进制的前两位用来生成目录,目录为二层!例如:1B2C3D4E5F,/1/B/保存文件。

  4. 上传文件的大小限制:
    1).单个文件大小限制
    sfu.setFileSizeMax(100*1024):限制单个文件大小为100KB
    上面的方法调用,必须在解析开始之前调用!
    如果上传的文件超出限制,在parseRequest()方法执行时,会抛出异常:FileUploadBase.FileSizeLimitExceededException

    2).整个请求所有数据大小限制
    sfu.setSizeMax(1024 * 1024):限制整个表单大小为1M
    这个方法也是必须在parseRequest()方法之前调用
    如果上传的文件超出限制,在parseRequest()方法执行时,会抛出异常:FileUploadBase.SizeLimitExceededException

  5. 缓存大小与临时目录:
    1).缓存大小:指超过多大值,才向硬盘保存!默认为10KB
    2).临时目录:向硬盘的什么目录保存
    3).设置缓存大小与临时目录:new DiskFileItemFactory(20*1024, new File(“F:/temp”))

二、文件下载

1.1 文件下载介绍

  下载就是向客户端响应字节数据,把一个文件变成字节数组,使用response.getOutputStream()来响应输出给浏览器。

1.2 文件下载的要求

  两个头一个流!

  1. Content-Type:
    表示传递给客户端的文件是什么MIME类型,例如:image/pjpeg
    可以通过文件名称调用ServletContext的getMimeType()方法,得到MIME类型。
  2. Content-Disposition:它的默认值为inline,表示在浏览器窗口中打开,attachment;filename=“文件名称”
  3. 流:要下载的文件数据!
    自己new一个输入流即可!
1.3 下载的细节

显示在下载框中的文件名称为中文时会出现乱码,处理中文乱码的通用方案:
filename = new String(filename.getBytes(“GBK”), “ISO-8859-1”);

1.4 下载示例
public class DownloadServlet extends HttpServlet {
	public void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		//本地文件路径
		String fileUrl = "D:/考拉.jpg";
		//所有浏览器都会使用本地编码,即中文操作系统使用GBK,浏览器收到这个文件名后,会使用iso-8859-1来解码
		String filename = new String("考拉.jpg".getBytes("GBK"), "ISO-8859-1");
		//通过文件名称获取文件的MIME类型
		String contentType = this.getServletContext().getMimeType(fileUrl);
		//获取文件资源的输入流
		FileInputStream input = new FileInputStream(fileUrl);
		//设置头
		response.setHeader("Content-Type",contentType);
		response.setHeader("Content-Disposition", "attachment;filename=" + filename);
		
		//响应输出流
		ServletOutputStream output = response.getOutputStream();
		//利用common-io工具类的copy方法把输入流中的数据写入到输出流输出
		IOUtils.copy(input, output);
		input.close();
	}
}