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



    目录


    文件上传概述

    实现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");

    代码

  • 相关阅读:
    ImageWatch 无法安装在VS2017环境下的解决方案
    Android CmakeList
    Android 工程越来越大,运行变卡解决方法
    奥卡姆剃刀(简约之法则)
    Cmake时 如何在windows命令行 选择vs版本
    ubuntu 18.04 安装tensorflow 2 cuda10 CUDNN Anaconda3
    Centos7简易通过yum安装phpmyadmin
    centos7 nigx 免费永久获取 Let‘s Encrypt 证书
    Execution failed for task ':app:compileDebugJavaWithJavac'
    centos 安装aconda
  • 原文地址:https://www.cnblogs.com/young-youth/p/11665695.html
Copyright © 2020-2023  润新知