文件上传
对于文件上传,浏览器在上传的过程中是将文件以流的形式提交到服务器端的,如果直接使用Servlet获取上传文件的输入流然后再解析里面的请求参数是比较麻烦,所以一般选择采用apache的开源工具common-fileupload这个文件上传组件。这个common-fileupload上传组件的jar包可以去apache官网上面下载,也可以在struts的lib文件夹下面找到,struts上传的功能就是基于这个实现的。common-fileupload是依赖于common-io这个包的,所以还需要下载这个包。
l 实现web开发中的文件上传功能,需完成如下二步操作:
- 在web页面中添加上传输入项
- 在servlet中读取上传文件的数据,并保存到服务器硬盘中。
l 如何在web页面中添加上传输入项?
- <input type=“file”>标签用于在web页面中添加文件上传输入项,设置文件上传输入项时须注意:
- 1、必须要设置input输入项的name属性,否则浏览器将不会发送上传文件的数据。
- 2、必须把form的enctype属值设为multipart/form-data.设置该值后,浏览器在上传文件时,将把文件数据附带在http请求消息体中,并使用MIME协议对上传的文件进行描述,以方便接收方对上传数据进行解析和处理。
- 3、表单的提交方式要是post
Fileupload组件上传原理:
由使用Apache文件上传组件(Fileupload)处理文件上传,通过DiskFileItemFactory工厂生成一个ServletFileupload文件上传解析器,
使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
通过遍历fileItem集合,通过isFormField()方法判断fileItem中封装的数据是普通字段还是上传的文件
如果是普通字段,就通过getFileName()获取上传的普通字段,通过getString()解决普通字段中文数据乱码问题
如果是上传的文件,就通过getInputStream()获取上传文件的输入流,通过getname获取上传的文件名
通过输出流将数据输出
l 实现步骤
1、创建DiskFileItemFactory对象,设置缓冲区大小和临时文件目录
2、使用DiskFileItemFactory 对象创建ServletFileUpload对象,并设置上传文件的大小限制。
3、调用ServletFileUpload.parseRequest方法解析request对象,得到一个保存了所有上传内容的List对象。
4、对list进行迭代,每迭代一个FileItem对象,调用其isFormField方法判断是否是上传文件
- True 为普通表单字段,则调用getFieldName、getString方法得到字段名和字段值
- False 为上传文件,则调用getInputStream方法得到数据输入流,从而读取上传数据。
l API详解
(1)DiskFileItemFactory 是创建 FileItem 对象的工厂
public DiskFileItemFactory(int sizeThreshold, java.io.File repository)
public DiskFileItemFactory()
public void setSizeThreshold(int sizeThreshold);字节为单位
设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。
public void setRepository(java.io.File repository)
指定临时文件目录,默认值为System.getProperty("java.io.tmpdir").
(2)ServletFileUpload处理上传的核心类
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 Listener)
实时监听文件上传状态
(3)FileItem用来表示文件上传表单中的一个上传文件对象或者普通表单对象
boolean isFormField() 判断FileItem是一个文件上传对象还是普通表单对象
如果判断是一个普通表单对象
String getFieldName() 获得普通表单对象的name属性
String getString(String encoding) 获得普通表单对象的value属性,可以用encoding进行编码设置
如果判断是一个文件上传对象
String getName() 获得上传文件的文件名(有些浏览器会携带客户端路径)
InputStream getInputStream() 获得上传文件的输入流
delete() 在关闭FileItem输入流后,删除临时文件
代码示例:
一、开发环境搭建
二、实现文件上传
2.1、文件上传页面和消息提示页面
upload.jsp页面的代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>文件上传</title> </head> <body> <form action="${pageContext.request.contextPath}/servlet/UploadServlet" enctype="multipart/form-data" method="post"> 文件名:<input type="text" name="filename"/> 文件名2:<input type="text" name="filename2"/> 文件:<input type="file" name="file"/> <input type="submit" value="上传"/> </form> </body> </html>
message.jsp页面:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>消息提示页面</title> </head> <body> ${message} </body> </html>
2.2、处理文件上传的Servlet
package org.fkjava.upload; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; 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.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; public class UploadServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { /**得到上传文件的保存目录,将上传的文件存放在WEB-INF目录下,不允许外界直接访问,保证文件的上传安全*/ String savePath = this.getServletContext().getRealPath("/WEB-INF/upload"); System.out.println(savePath); File file = new File(savePath); /**判断上传文件的保存目录是否存在*/ if(!file.exists() && !file.isDirectory()){ System.out.println(savePath+"目录不存在,需要创建"); /**创建目录*/ file.mkdir(); } /**消息提示*/ String message=""; try { /**使用Apache的文件上传组件处理文件上传步骤:*/ /**1、创建一个DiskFileItemFactory工厂*/ DiskFileItemFactory factory = new DiskFileItemFactory(); /**2、创建一个文件上传解析器*/ ServletFileUpload upload = new ServletFileUpload(factory); /**解决上传文件名的中文乱码*/ upload.setHeaderEncoding("utf-8"); /**3、判断提交的数据是否是上传表单的数据(判断是否是enctype="multipart/form-data"上传方式)*/ if(ServletFileUpload.isMultipartContent(request)){ /**4、使用ServletFileUpload解析器解析上传的数据,解析结果返回的是一个List<FileItem>集合, * 每一个FileItem对应一个Form表单的输入项*/ List<FileItem> list = upload.parseRequest(request); for(FileItem item : list){ /**如果fileItem中封装的是普通输入项的数据*/ if(item.isFormField()){ String name = item.getFieldName(); /**解决普通输入项的数据中文乱码问题*/ String value = item.getString("utf-8"); System.out.println(name + ":" + value); }else{ /**如果fileItem中封装的是上传文件*/ String filename = item.getName(); System.out.println(filename); if(filename==null || filename.trim().equals("")){ continue; } /** *处理IE6的bug,提交上来的文件名是带有路径的,如: c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt *处理获取到的上传文件的文件名的路径部分,只保留文件名部分 *filename.substring(filename.lastIndexOf("\\")+1)表示截取到最后一个\的后一个索引之后的字符串 * */ filename = filename.substring(filename.lastIndexOf("\\")+1); System.out.println(filename); /**获取item的上传文件的输入流*/ InputStream in = item.getInputStream(); /**创建一个文件输出流*/ FileOutputStream out = new FileOutputStream(savePath + "\\" + filename); /**创建一个缓冲区*/ byte[] buffer = new byte[1024]; /**判断输入流中的数据是否已经读完的标识*/ int len = 0; /**循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据*/ while((len=in.read(buffer))>0){ out.write(buffer, 0, len); } //关闭输入流 in.close(); //关闭输出流 out.close(); //删除处理文件上传时生成的临时文件 item.delete(); message = "文件上传成功"; } } }else{ /**不是enctype="multipart/form-data"上传方式,按传统方式获取数据)*/ return; } } catch (Exception e) { message = "文件上传失败!"; e.printStackTrace(); } request.setAttribute("message", message); request.getRequestDispatcher("/message.jsp").forward(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
在Web.xml文件中注册UploadServlet:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1"> <display-name>FileUploadAndDownLoad</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>uploadServlet</servlet-name> <servlet-class>org.fkjava.upload.UploadServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>uploadServlet</servlet-name> <url-pattern>/servlet/UploadServlet</url-pattern> </servlet-mapping> <servlet> <servlet-name>uploadServlet2</servlet-name> <servlet-class>org.fkjava.upload.UploadServlet2</servlet-class> </servlet> <servlet-mapping> <servlet-name>uploadServlet2</servlet-name> <url-pattern>/servlet/UploadServlet2</url-pattern> </servlet-mapping> </web-app>
2.3、文件上传的细节
上述的代码虽然可以成功将文件上传到服务器上面的指定目录当中,但是文件上传功能有许多需要注意的小细节问题,以下列出的几点需要特别注意的
1、为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如放于WEB-INF目录下。
2、为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名,前面拼上UUID值。
3、为防止一个目录下面出现太多文件,要使用hash算法打散存储。
4、要限制上传文件的最大值。
5、要限制上传文件的类型,在收到上传文件名时,判断后缀名是否合法。
上传过程的监听
一共要上传的字节数:
当前已经上传的字节:
当前已经上传的百分比:
当前已经上传的时间:
传输速度:
剩余时间数:
针对上面的细节改进代码:
package org.fkjava.upload; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; 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 UploadServlet2 extends HttpServlet{ private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("---01"); /**得到上传文件的保存目录,将上传的文件存档于WEB-INF下,不允许外界直接访问,保证上传文件的安全*/ String savePath = this.getServletContext().getRealPath("/WEB-INF/upload"); System.out.println(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); //判断是否是enctype="multipart/form-data"上传方式 if(ServletFileUpload.isMultipartContent(request)){ //设定初始时间 final Long timeStamp = System.currentTimeMillis(); //注册上传监听器 upload.setProgressListener(new ProgressListener() { @Override public void update(long bytesRead, long contentLength, int items) { try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } //一共要上传的字节 //已经上传字节 //当前已经上传的百分比 BigDecimal decimal = new BigDecimal(bytesRead)
.divide(new BigDecimal(contentLength), 4, BigDecimal.ROUND_HALF_UP).multiply(new BigDecimal(100));
System.out.print("当前读取到的字节数:"+bytesRead+",文件总字节数:"+contentLength+",当前上传的文件在表单中的位置:"+items+",
当前已经上传的百分比:"+decimal.doubleValue()+"%"); //当前已经上传的时间: Long time = (System.currentTimeMillis()-timeStamp)/1000; System.out.println("已经用时"+time+"秒"); //传输速度(字节/秒)=当前上传的字节/所用时间 if(time == 0)return; BigDecimal speed = new BigDecimal(bytesRead).divide(new BigDecimal(time),2,BigDecimal.ROUND_HALF_UP)
.divide(new BigDecimal(1024),2,BigDecimal.ROUND_HALF_UP);
System.out.print("上传速度"+speed.doubleValue()+"KB/S"); //大约剩余时间数 BigDecimal remainTime = new BigDecimal((contentLength-bytesRead)/1000).divide(speed,2,BigDecimal.ROUND_HALF_UP); System.out.println("大约剩余"+remainTime+"秒。。。"); System.out.println(); } }); //解决上传文件名的中文乱码 upload.setHeaderEncoding("UTF-8"); //设置单个上传文件的大小,这里设置为1024*1024字节,也就是1M upload.setFileSizeMax(1024*1024*5); //设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,这里设置为10M upload.setSizeMax(1024*1024*10); /**4、使用ServletFileUpload解析器解析上传的数据,解析结果返回的是一个List<FileItem>集合, * 每一个FileItem对应一个Form表单的输入项*/ List<FileItem> list = upload.parseRequest(request); for(FileItem item : list){ //如果fileItem中封装的是普通输入项的数据 if(item.isFormField()){ //获取数据 String name = item.getFieldName(); //解决普通输入项的数据中的中文乱码 String value = item.getString("UTF-8"); System.out.println(name + "---" + value); }else{ //如果fileItem中封装的是上传文件 //获取上传的文件名 String fileName = item.getName(); System.out.println("上传的文件名是:"+fileName); if(fileName==null || fileName.trim().equals("")){ continue; } /** *处理IE6的bug,提交上来的文件名是带有路径的,如: c:\a\b\1.txt,而有些只是单纯的文件名,如:1.txt *处理获取到的上传文件的文件名的路径部分,只保留文件名部分 *filename.substring(filename.lastIndexOf("\\")+1)表示截取到最后一个\的后一个索引之后的字符串 * */ fileName = fileName.substring(fileName.lastIndexOf("\\")+1); System.out.println("截取后的文件名:"+fileName); //得到上传文件的扩展名 String fileExtName = fileName.substring(fileName.lastIndexOf(".")+1); //如果需要限制上传的文件类型,那么可以通过文件的扩展名来判断上传的文件类型是否合法 System.out.println("上传的文件的扩展名是:"+fileExtName); /**获取item的上传文件的输入流*/ InputStream in = item.getInputStream(); //得到文件保存的名称,创建makeFileName()方法,使用UUID确保上传的文件名唯一,避免覆盖 String saveFileName = makeFileName(fileName); //得到文件的保存目录,使用hash算法打散存储,防止一个文件夹下的内容太多 String realSavePath = makePath(saveFileName,savePath); /**创建一个文件输出流*/ FileOutputStream out = new FileOutputStream(realSavePath + "\\" + saveFileName); /**创建一个缓冲区*/ byte[] buffer = new byte[1024]; /**判断输入流中的数据是否已经读完的标识*/ int len = 0; /**循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据*/ while((len=in.read(buffer))>0){ //使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + "\\" + filename) out.write(buffer, 0, len); } //关闭输入流 in.close(); //关闭输出流 out.close(); //删除处理文件上传时生成的临时文件 item.delete(); message = "文件上传成功"; } } }else{ //按照传统方式获取数据 return; } }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); } /** * @Method:makeFileName * @param fileName 文件的原始名称 * @return uuid + "_" + 文件的原始名称 */ private String makeFileName(String fileName) { //防止文件覆盖,确保上传的文件名唯一 return UUID.randomUUID().toString() + "_" + fileName; } /** * 为防止一个目录下面出现太多文件,要使用hash算法打散存储 * @param saveFileName 文件名,需要根据文件名生成存储目录 * @param savePath 文件的存储路径 * @return 新的存储路径 */ private String makePath(String fileName, String savePath) { //得到文件名的hashCode值,得到的就是fileName这个字符串对象在内存中的地址 int hashCode = fileName.hashCode(); int dir1 = hashCode & 0xf; int dir2 = (hashCode & 0xf0)>>4; //构造新的保存目录 String dir = savePath + "\\" + dir1 + "\\" + dir2; //upload\1\2 File file = new File(dir); //如果目录不存在,就创建 if(!file.exists()){ file.mkdirs(); } return dir; } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }