文件上传
一、概述
- 在web开发中经常需要从客户端向服务端上传文件 , 如: 照片 、 文件 , 这些通常都需要web开发中的文件上传技术实现。
二、 文件上传开发步骤
- 提供一个带有文件上传项的表单
- 文件上传的输入框必须有name属性才能被上传
- 文件上传的表单必须是Post提交 , 因为Get请求对参数的格式有种种限制如:限制大小为4kb , 只能传输简单字符。。。
- 文件上传的表单必须设置enctype=mulipart/form-data
- 在servlet中处理文件上传相关的逻辑方案
- 方式一 : 可以通过JavaEE原生的API来获取请求中使体内容的流
- 获取到流中的数据在进行解析处理
- 但是过于繁琐 , 不推荐使用
- 方式二: 使用开源工具实现
- 使用开源工具实现 Apache提供的文件上传包 – Commons-fileupload
相关逻辑的开发
- 下载并导入对应的包
- Commons-fileupload
- Commons-io
- 扩展: POI Java代码操作office的开源组件
相关类详解
- 文件上传工厂DiskFileItemFactory
- public DiskFileItemFactory(int sizeThreshold , java.io.File repository)
- sizeTHreshold :指定内存缓冲区大小
- repository :指定临时文件存放位置
- 注意:
- 文件上传时需要将请求的实体内容全部读取后才做处理
- 此时需要将实体内容缓冲起来。 内存缓冲快 , 但是内存本身就不大 , 容易导致系统崩溃 。 文件缓冲慢 , 但是可以缓冲大量数据 。
- 所以此处提供了两个选项 ,用sizeTHreshold设置内存允许缓冲的最大 数据量 。 如果数据量大于sizeThreshold ,则使用文件缓冲 , 在repository指定的目录下创建临时文件来缓冲 。 如果数据小于sizeThreshold , 则使用内存缓冲
- public DiskFileItemFactory(int sizeThreshold , java.io.File repository)
- 文件上传的核心类ServletFileUpload
- ServletFileUpload sfl = new ServletFileUpload(factory);
- boolean isMultipartContent(HttpRequest request)判断当前上传的表单是否是enctype=mulipart/form-data格式
- setHeaderEncoding(String encodeing): 指定文件处理时使用的编码
- setFileSizeMax(long filesizemax):控制单个文件上传的最大大小值
- setSizeMax(long sizeMax):控制总的文件上传的最大值
- List parseRequest(HttpServeltRequest request):解析request对象 获取FileItem集合
- setProgressListener设置上传文件的监听
文件上传项 FileItem
List<FileItem> parseRequest(HttpServeltRequest request)
- 遍历Item分别做处理
- 判断下一个Item是不是一个普通字段
- isFormField():如果返回true , 则是普通字段
- getFieldName()获取字段名
- getString(String encoding):获取字段的值 , 可以再获取字段值得时候设置字段值的编码集
- 如果是文件上传项
- getName():获取文件名
- getInputStream()获取文件流
- isFormField():如果返回true , 则是普通字段
- delete():遍历完之后 记得删除临时文件
- 文件上传工厂DiskFileItemFactory
- 下载并导入对应的包
三、文件上传时需要注意的点
ie兼容
- 当用户使用ie浏览器上传文件时 , 通过getName()获取的是该文件在客户端主机 里的绝对路径 , 而其他浏览器使用getName()时获取的仅仅是文件名 , 在创建目录时我们需要在真实的文件名前添加服务器文件存放的路径 ,所以ie浏览器上传的文件如果不对文件名做处理则会服务器爆出路径无效异常
处理:
if(name.contians("\\")){ name = name.subString(name.lastIndexOf("\\")); }
- 文件上传保存位置问题
- 文件上传保存位置一定不能被 外界随意访问 , 防止用户用浏览器直接访问下载服务器资源 或者执行恶意代码。
- WEB-INF目录时不能被外界直接访问的 , 文件上传之后要么保存在WEB-INF目录下 , 要目放置在本地磁盘的其他位置 , 保证浏览无法直接访问 。
- 文件上传重名问题
- 在同一路径下操作系统不允许文件重名 , 如果有文件重名 ,注入后来的文件会直接覆盖原有的文件 。
- 在文件保存时在文件名之前拼接UUID来确保文件名绝对唯一
- 扩展:UUID是基于系统时间、CPU时钟等参数使用哈希算法得出的值 , 是永远不会重复的 。
- 上传文件保存目录保存文件过多时系统处理困难导致无法读取的情况
- 一个文件夹下保存的文件过多时 , 会造成访问缓慢 , 甚至可能造成无法访问的情况 , 所以要想办法将上传的文件分目录存储
- 利用hash算法的散列特性
- 将带有UUID的文件名根据散列算法得出一个32位的二进制字符串后转为16进制的8位字符
- 将得到的8位字符的字符串的每一位截取出来都作为一级路径
- 最终文件存放在8级的hash目录下, 一共可能有16^8(42亿多一点)个文件夹分目录来存放文件 , 从而保证了每一个目录都不会存放过多的文件
示例:
public class UploadServlet extends HttpServlet { public void doGet(final HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { response.setCharacterEncoding("utf-8"); response.setContentType("text/html;charset=utf-8"); //1.创建文件上传工厂 DiskFileItemFactory factory = new DiskFileItemFactory(100, new File(this.getServletContext().getRealPath("WEB-INF/temp"))); //2.创建文件上传的核心类 ServletFileUpload fileUpload = new ServletFileUpload(factory); //--判断当前文件上传的表单是否满足enctype=multipart/form-data if(!fileUpload.isMultipartContent(request)){ throw new RuntimeException("请使用正确的文件上传表单上传数据!"); } //--设置文件名解析时采用的编码 fileUpload.setHeaderEncoding("utf-8"); //--单个文件不能超过1MB //fileUpload.setFileSizeMax(1024 * 1024); //--总大小不能超过10MB //fileUpload.setSizeMax(10 * 1024 * 1024); //--设置上传文件的监听 fileUpload.setProgressListener(new ProgressListener(){ long begin = System.currentTimeMillis(); public void update(long pBytesRead, long pContentLength,int pItems) { // System.out.print("正在读取第"+pItems+"个上传项。。"); // System.out.print("共"+pContentLength+"字节。。"); // System.out.print("已经读取了"+pBytesRead+"字节。。"); long leftBytes = pContentLength - pBytesRead; // System.out.print("剩余"+leftBytes+"字节。。"); long now = System.currentTimeMillis(); long usetime = (now - begin)/1000 ; // System.out.print("已经用时" + usetime+"秒。。"); long speed = usetime == 0 ? 0 : pBytesRead / usetime / 1024; // System.out.print("上传速度" + speed+"KB/s。。"); double per = Math.round(pBytesRead * 10000.0 / pContentLength)/100.0; System.out.println("上传百分比" + per + "%。。"); long lefttime = speed == 0 ? 0 : leftBytes /1024 / speed; // System.out.println("大致剩余时间"+lefttime+"秒"); request.getSession().setAttribute("progress", per); } }); //--解析request得到FileItem的集合 List<FileItem> items = fileUpload.parseRequest(request); //--遍历每个item分别做处理 for(FileItem item : items){ if(item.isFormField()){ //普通字段项 String name = item.getFieldName(); String value = item.getString("utf-8"); System.out.println(name+"~"+value); }else{ //文件上传项 String fname = item.getName(); //--处理ie文件名bug if(fname.contains("\\")){ fname = fname.substring(fname.lastIndexOf("\\")+1); } //--处理文件名 使其不会重复 String savename = UUID.randomUUID().toString()+"_"+fname; //--文件分目录处理 //----获取文件名的hash 转换为16进制字符串表现形式 由于文件名随机 所以 hash值也是散列的 String hash = Integer.toHexString(savename.hashCode()); //----如果hash不足8位则在前面拼接足够8位的0 while(hash.length()<8){ hash += "0"; } //----遍历hash值字符串的每一个字符作为一级目录拼接 String savepath = "/WEB-INF/upload/"; for(int i = 0;i<hash.length();i++){ savepath += (hash.charAt(i)+"/"); } //----创建出该目录 new File(this.getServletContext().getRealPath(savepath)).mkdirs(); //----得到输入流 InputStream in = item.getInputStream(); //----得到输出流 路径就是上面 hash拼接出的路径+文件名 OutputStream out = new FileOutputStream(this.getServletContext().getRealPath(savepath + "/" +savename)); //----输出数据到文件 byte [] data = new byte[1024]; int n = -1; while((n = in.read(data))!=-1){ out.write(data,0,n); } //----关闭流 in.close(); out.close(); //----删除缓存文件 item.delete(); } } } catch (FileSizeLimitExceededException e) { response.getWriter().write("文件大小超过限制!!"); } catch (Exception e) { throw new RuntimeException(e); } } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
四、文件上传进度条的实现
- 在Upload.jsp页面中 ,为表单提交时间增加函数触发,增加定时器 , 每隔一段时间通过Ajax向服务器发送请求获取最新的上传进度
- 在UploadServlet中为文件上传注册监听器 , 计算各种指标 , 存入session中
- 在UploadProgressServlet中从session中获取上传进度信息 , 作为响应发回给Ajax后设置进度条长度。