(二十六)文件上传和下载


目录


文件上传概述

实现web开发中的文件上传功能,需要完成如下二步操作:

  1. 在web页面中添加上传输入项
  2. 在servlet中读取上传文件的数据,并保存到本地硬盘中

如何在web页面中添加上传输入项

  • <input type='file'> 标签用于在web页面中添加上传输入项,设置文件上传输入项的时需要注意:

    1. 必须要设置 input 输入项的 name 属性,否则浏览器将不会发送上传文件的数据。这就是之前发现的那个,如果一个标签没有 name 属性,是不会被提交的 。

    2. 必须把 formenctype 属性设置 multipart/form-data , 设置该值后,浏览器在上传文件时,把文件数据附带在 http 请求消息体 中,并使用 MIME 协议对上传的文件进行描述,以方便接收方对上传数据进行解析和处理 。

    3. 数据格式:这种表单提交,和以前的表单提交不一样,比如以前获取用户名,直接获取提交参数,就好了;使用了 MIME 协议以后,提交的数据格式将不再和以前一样,格式如下所示,因此,要获取其中的额数据,我们需要去使用特定的解析 ;

                  -----------------------------24464570528145
                  Content-Disposition: form-data; name="username"
      
                  暗色调
      
                  -----------------------------24464570528145
                  Content-Disposition: form-data; name="file"; filename="duola.jpg"
                  Content-Type: image/jpeg
      
                  ÿØÿà-sßÉKX‡é/ü3î·ÿ(文件数据,二进制,这里显示成乱码了)
      
      
                  -----------------------------24464570528145
                  Content-Disposition: form-data; name="file2"; filename=""
                  Content-Type: application/octet-stream
      
      
                  -----------------------------24464570528145--       


如何在 servlet 中读取文件上传数据,并保存到本地硬盘中?

  • Request 对象提供一个 getInputStream 方法,通过这个方法可以获取客户端提交过来的数据。
  • 但是由于用户可能会同时上传多个文件,在 servlet 端编程直接读取上传的数据,并解析出相应文件的数据是一件非常麻烦的工作。
  • 为了方便处理用户上传数据,Apache 开源组织提供了一个用来处理表单上传文件的一个开源组件(Commons-fileupload),该组件性能优异,并且 API 使用简洁 ;
  • 使用 Commons-fileupload 组件实现文件上传,需要导入该组件相应的jar包:

        ·Commons-fileupload 
    
        ·Commons-io;(虽然它不属于上传文件的开发jar,但是上传文件的jar包依赖于它)
    

fileupload组件工作流程 ;

        1、首先 IE 端,会将表单的数据封装到 request 里面,提交给服务器;

        2、组件提供一个工厂 DiskFileItemFactory ,通过工厂获取 ServletFileUpload ,

            ServletFileupLoad 内部也是通过 getInputStream 。

        3、调用 ServlFileupLoad 的parseRequest 方法解析数据, 解析完数据以后,将数据封装在一个 FileItem 类型的list中,

            调用 FileItem 的 isFormField 方法,判断其是基本字段还是文件上传(返回TRUE 就是基本字段);

        4、如果是基本字段,则调用 getFieldName 获取字段的名字,调用 getString 获取字段的值 ;

        5、如果是上传文件,则调用 getName 获取上传文件的名字。

            (获取的名字,根据浏览器的版本决定,有的获取全部名字,有的获取最后的名字),因此,我们要截取一下 。

            调用 getInputStream 读取文件 ;

上传细节之乱码问题

  • 乱码问题

    1. 文件名乱码问题

      因为解析器是老外写的,里面的内置的编码是拉丁码表,因此,在读取中文的时候,会出现乱码;

      即使我们设置了request的码表,也是无济于事的,对此,我们需要设置下解析器的码表 ;

    2. 普通字段乱码问题

      这里由于表单类型是 Multipart ,即使我们设置了request的编码,也是无济于事的,因为,问题出在,getString() 方法上,它内部使用了拉丁码表

      要想改变它,用getString(“码表”) 的重载方法 ;


上传细节之验证表单类型

  • 验证表单类型

    1. 在开始操作之前,先判断下表单类型,ServletFileUpload.isMultipartContent(request)

      返回 true 表示是上传表单 ;用于检测是否有人搞破坏!

上传细节之缓冲区问题

  1. 文件大小、缓冲区问题

    1. 上传文件特别大情况,设置 缓冲区大小临时文件位置 ,临时文件需要自己设置自动删除 ;

    2. public void setSizeThreshold(int sizeThreshold) ;

    3. 设置内存缓存区大小,默认为 10K 。当上传文件大于缓冲区大小的时候,fileupload 组件将使用临时文件缓存上传文件 ;

      也就是说,当文件大小小于缓冲区的时候,使用缓冲区保存文件;如果临时文件大于缓冲区大小,则不使用缓冲区,而是先将文件写到临时文件里面保存 ;

      也就是说,当文件小于缓冲区大小的时候,fileupload组件的内置缓冲区作为一个中转站,先将文件写到缓冲区中,再从缓冲区写到服务器上 ;

      如果文件大小大于缓冲区大小,则不再使用缓冲区作为中转站,而是使用临时文件作为中转,先将客户端的文件写到fileupload组件一个临时文件中,再从临时文件中写到服务器上 ;

      备注:为什么需要中转

      原因很简单,中转说白了,就是一个缓冲区,我们将文件从文本上传到服务器,如果不使用缓冲区,直接从本地到服务器,每次磁头读取一次,然后送到服务器,然后磁头再次读取,循环往复;而每次磁头读取硬盘都是一个耗时的工作,我们应该减少磁头读取硬盘次数,或者说磁头读取一次硬盘,尽量的多读取数据,从而减少读取的次数,这样我们就需要,一个中转,让磁头一次读取更多的数据,然后保存到缓冲中;

      如果没有中转当缓冲,那么一次磁头读取一个字节以后,就必须停止,待数据送达服务器端,才可以再次读取;这显然是不合理的,因此,我们需要缓冲区!

    4. 设置临时文件位置

    //            设置临时文件的保存位置 ;
            factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));

    使用 public void setRepository(java.io.File respository),指定临时文件位置,参数接受一个文件路径,这里我们是在JEE中使用File,因此,需要得到文件的绝对路径,而不是相对路径;

    使用servletContext 获取文件真实路径

    指定临时文件目录,默认值是 System.getProperty("java.io.tmpdir") ;

    临时文件需要我们删除掉,使用item.delete()方法 ;

    注意删除方法,要放在关闭流之后,否则还有流与文件关联,是删不掉的 ,确保被删掉,还需要放在finally里面;


上传细节之文件分配(保存目录)

对于保存上传文件,文件的保存目录,是绝对不能被外界访问到的;否则上传一段JSP文本,然后访问这个jsp文件,JSP就会被执行!

这个目录的地址,需要好好写,不要在获得真实路径的字符串最后加上 / ,路径和名字,中间用 路径分隔符 拼接,使用File.sep... 来适应不同的系统 ;

其中IDEA,在上传文件的时候,web项目中的文件夹下,是没有文件产生的,产生的文件在Out文件下 ;

文件的保存路径,我们必须放在WEB文件夹下面,还有一个问题需要解决,windows操作系统,每个文件夹下面的直接文件数量超过1000的时候,打开该文件,就会卡顿,文件夹中文件夹中的数量,不算在直接文件数量中

因此,我们需要设计出一个算法,来分配上传的文件;这里,我们根据文件名的哈希码来分配文件位置,哈希码是一个int值,一共32位,每4位为一个层次,生成文件夹,最多可以生成8层,可以保存上兆的文件了。;

算法思想

获取文件名的哈希码,每4位二进制位,作为一个整数位;

第一个四位,就可以生成0-15号文件夹;

第二个四位,又会生成0-15号文件夹;

它们嵌套在一起,就可以存储16*16*1000个文件;

...第三个、第四个四位...

根据我们的需要,我们一共可以生成8层文件夹嵌套;

我用计算器算了一下,至少可以保存 ‭4294967296000‬ 这么多的文件,都到达兆的级别了,妥妥够用了;

每次保存文件的时候,先取文件名的哈希值,看对应的文件夹是否存在,

不存在就创建出文件路径返回,存在就直接返回路径 ;
/**
     * 随机打乱文件存储位置,文件分配
     *
     * @param path
     * @param name
     * @return
     */
    public String generateRandomPath(String path, String name) {
//         根据名字的哈希码
        int hashcode = name.hashCode();
//        用哈希码的每4位 生成一个文件夹,我们这里嵌套3层 文件夹,可以最多保存 16 * 16 * 16 * 1000

//       获取低四位
        int one = hashcode & 0xf;
//       获取低 五 - 八 位
        int two = (hashcode >> 4) & 0xf;
//        获取低 九 - 十三 位
        int three = (hashcode >> 4 >> 4) & 0xf;

//        生成路径。需要先判断,路径是否存在 ;
        File file = new File(path + File.separator + one + File.separator + two + File.separator + three);
//         判断路径是否存在
        if (!file.exists()) {
//            创建多级目录
            file.mkdirs();
        }

        return file.getPath();
    }

上传细节之限制上传文件类型

在处理文件的时候,先获取文件的后缀名,做个判断,看是否在我们允许的文件类型之中 ;

     String suffix = name.substring(name.lastIndexOf("."));

//       对文件后缀进行判断
//       limits是一个集合,里面保存着我们允许的后缀名
         if (!(limits.contains(suffix))) {
         request.setAttribute("message", "暂不支持 " + suffix + "文件的上传。。");
         request.getRequestDispatcher("/WEB-INF/jsp/Message.jsp").forward(request, response);
         return;
                    }

上传细节之限制上文文件大小

//            在解析器上设置
//            设置上传文件大小限制为500M
            upload.setFileSizeMax(1024 * 1024 * 500);

上传细节之客户端没有文件上传

如果文件没有上传,则服务器端获取 文件名 的时候是 空串


//                    获取输入流
                    InputStream input = item.getInputStream();
                    int len = 0;
                    byte[] bytes = new byte[1024];
//                    获取上传文件的名字 如果没有文件上传,则获取空串
                    String name = item.getName();

//                    没有上传文件
                    if (name.trim().equals("")) {
                        continue;
                    }

上传细节之文件名相同的问题

利用UUID 生成随机ID ,再加上原本文件的名字,来防止文件名冲突的问题;

//                    对名字进行截取,以适应不同的浏览器
                    name = name.substring(name.lastIndexOf("\") + 1);
//                      生成随机名字
                    name = generateUuuidName(name);

上传细节之动态添加上传文件项

JSP中利用JS代码,动态的添加、删除 ;

JS代码:

<script type="text/javascript">
    function add() {
        var input = document.createElement("input");
        input.type = "file";
        input.name = "file";
        var btn_del = document.createElement("input");
        btn_del.type = "button";
        btn_del.value = "删除" ;
        btn_del.onclick = function del() {
            //this 谁触发了这个方法,this 就是谁
            this.parentNode.parentNode.removeChild(this.parentNode) ;
        };

        var div = document.createElement("div");
        div.appendChild(input);
        div.appendChild(btn_del);

    // 添加到已经存在的节点上,appendChild添加,默认作为最后一个孩子节点
        var uploadfile = document.getElementById(" uploadFile");
        uploadfile.appendChild(div);
    }

</script>

上传细节之进度条

为解析器注册一个监听器,解析器里面有方法,当有数据被解析的时候,会被调用 ;

根据这个方法的参数,可以获取当前上传文件的进度 ;

//  解析器要在开始解析之前,进行注册!!!

// 实现没上传 1 M 进度条更新一下 ;

ProgressListener progressListener = new ProgressListener(){
   private long megaBytes = -1;
   public void update(long pBytesRead, long pContentLength, int pItems) {

 // 已传输字节 是否 达到 1M
       long mBytes = pBytesRead / 1000000;  

//  只有传输字节比之前大于1 M ,才能更新进度条
       if (megaBytes == mBytes) {
           return;
       }

// 保存已经传输的字节以1M的比值,初始为 -1 
       megaBytes = mBytes;


       if (pContentLength == -1) {
           System.out.println("So far, " + pBytesRead + " bytes have been read.");
       } else {
           System.out.println("So far, " + pBytesRead + " of " + pContentLength
                              + " bytes have been read.");
       }
   }
};

上传细节之超链接中文

超链接中有中文的时候,需要URL编码 ;

在JSP 标签中,我们使用 < c : url > 标签,来完成URL编码 ;

resquest 设置编码,对get方法提交的数据无效 ;

        <c:url var="url" value="/downloadServlet">
            <%--会自动的进行url编码--%>
            <c:param name="fileName" value="${entry.value}"></c:param>
        </c:url>
        ${entry.key} <a href="${url}">下载</a><br>

下载细节之文件名含有空格、文件名乱码


            String path = file.getSavePath();
            String simpleName = file.getSimpleName();

//            浏览器分为 火狐 和 非火狐
            if (request.getHeader("USER-AGENT").toLowerCase().indexOf("firefox") >= 0) {
                // 火狐头大,需要独特设置一下
                simpleName = new String(simpleName.getBytes("UTF-8"), "iso-8859-1");
            } else {
                simpleName = URLEncoder.encode(simpleName, "UTF-8");
//                IE 文件名有空格会被加号代替。需要自己替换回去
                simpleName = simpleName.replaceAll("\+","%20");
            }
//            文件名有空格。火狐则会截断文件名,需要将文件名用字符串包起来
            //        告诉浏览器下载方式打开.,
            response.addHeader("Content-Disposition", "attachment;filename="" + simpleName+""");

下载步骤

  1. 递归列出服务器的所有文件,数据量大的情况下,注意分页 ;
  2. JSP中显示文件的时候,注意URL编码;
  3. 点击下载的时候,将文件的UUID带过去;
  4. servlet中,根据UUID获取,要下载的文件对象,这个文件对象中封装该文件的信息;
  5. 检查判断文件是否存在;
  6. 文件存在,则进行文件名的编码,这里注意 Firefox 和 其他浏览器的不同点;
  7. 对文件名中的空格,进行特殊处理;

    //   IE 文件名有空格会被加号代替。需要自己替换回去
                simpleName = simpleName.replaceAll("\+","%20") ;
    //   文件名有空格。火狐则会截断文件名,需要将文件名用字符串包起来
    //   告诉浏览器下载方式打开.,
            response.addHeader("Content-Disposition", "attachment;filename="" + simpleName+""");
  8. 告诉浏览器下载方式打开;

  9. 最后,获取response的输出流,将文件写给浏览器

ServletFileUpload 核心API


// 判断上传表单是否为 mulipart/form-data 类型 ;
boolean isMultipartContent(HttpServletRequest request) ;

// (限制单个文件大小)单位是K
setFileSizeMax(long fileSizeMax)

// 设置上传文件的最大值,如果超过大小现在,则抛出异常,异常是一个内部类异常 FileUploadBase.FileSizeLimitExceededException


//设置上传文件总量的最大值
// (限制多个文件大小)
setSizeMax(long sizeMax) 

//                  设置编码格式
//                    获取字段的值,设置码表
                    String value = item.getString("utf-8");
//                   设置解析器码表,解决文件名乱码
                    upload.setHeaderEncoding("utf-8");

代码

原文地址:https://www.cnblogs.com/young-youth/p/11665695.html