Java Web 之文件上传与下载

本文包括:

1、文件上传概述

2、利用 Commons-fileupload 组件实现文件上传

3、核心API——DiskFileItemFactory

4、核心API——ServletFileUpload

5、核心API——FileItem

6、拓展——使用 JavaScript 生成多个动态上传输入项

7、文件上传的细节处理问题

8、文件上传进度监听器——ProgressListener

9、文件下载概述

10、深度优先搜索与广度优先搜索

11、文件下载的细节处理问题

1、文件上传概述

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

    1. 在 jsp 页面中添加上传输入项

    2. 在servlet中读取上传文件的数据,并保存到服务器硬盘中

第一步:

  • 如何在 jsp 页面中添加上传输入项?

    • <input type="file">标签用于在 jsp 页面中添加文件上传输入项,设置文件上传输入项时须注意:

      1. 必须要设置 input 输入项的 name 属性,否则浏览器将不会发送上传文件的数据。

      2. 必须把 form 的 enctype 属性值设为 multipart/form-data 。其实 form 表单在你不写 enctype 属性时,也默认为其添加了 enctype 属性值,默认值是 enctype="application/x- www-form-urlencoded" 设置该值后,浏览器在上传文件时,将把文件数据附带在 http 请求消息体中,并使用 MIME 协议对上传的文件进行描述,以方便接收方对上传数据进行解析和处理。

        关于 MIME 协议请参考《Java Web之JavaMail》:http://www.jianshu.com/p/141c0f9b9c9e

      3. 表单的提交方式必须是post,因为上传文件可能较大。

        get:以【明文】方式,通过URL提交数据,数据在URL中可以看到。提交数据最多不超过【2KB】。安全性较低,但效率比post方式高。适合提交数据量不大,且安全要求不高的数据:比如:搜索、查询等功能。

        post:将用户提交的信息封装在HTML HEADER内,数据在URL中【不能看到】适合提交数据量大,安全性高的用户信息。如:注册、修改、上传等功能。

        区别:

        1. post隐式提交,get显式提交。

        2. post安全,get不安全。

        3. get提交数据的长度有限(255字符之内),post无限。

    • 示例:

            <form action="xx.action" method="post" enctype="multipart/form-data">
            </form>
      

第二步

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

    • Request 对象提供了一个 getInputStream 方法,通过这个方法可以读取到客户端提交过来的数据(具体来说是 http 的请求体 entity)。但由于用户可能会同时上传多个文件,在 Servlet 端编程直接读取上传数据,并分别解析出相应的文件数据是一项非常麻烦的工作。

    • 具体工作:假设我们获取了 http 的请求体,如何得到上传的文件呢?如图:左边是 http 的请求体,右边是步骤:

      文件上传原理思想
    • 我们来实际操作一下到底是怎么工作的:

      1. 首先我们先写个简单的JSP页面,代码如下:

          <form action="/day20/upload" method="post" enctype="multipart/form-data">
             用户名 <input type="text" name="username"/><br/>
             上传文件 <input type="file" name="upload"/><br/>
             <input type="submit" name="submit" value="提交"/>
          </form>
        
      2. 然后打开 Tomcat ,填写用户名,选择上传一个 md 文件,如下图所示:

      3. md 文件内容如图所示:

      4. 在 Servlet 中的代码如下:

         public class UploadServlet extends HttpServlet {
         
             public void doGet(HttpServletRequest request, HttpServletResponse response)
                     throws ServletException, IOException {
                  request.setCharacterEncoding("utf-8");// 没法解决乱码,因为这是针对URL编码 解决乱码
                  // request提供 getInputStream方法,用来获得请求体信息
                  InputStream in = request.getInputStream();
                  int temp;
                  while ((temp = in.read()) != -1) {
                  System.out.write(temp);
                  }
                  System.out.flush();
                  in.close();
             }
         
             public void doPost(HttpServletRequest request, HttpServletResponse response)
                     throws ServletException, IOException {
                 doGet(request, response);
             }
         
         }
        
      5. 点击提交按钮,在控制台可以看到如下输出内容:

        <pre>
        ------WebKitFormBoundaryRdlJBEfBAruajkfg
        Content-Disposition: form-data; name="username"

        edisonleolhl
        ------WebKitFormBoundaryRdlJBEfBAruajkfg
        Content-Disposition: form-data; name="upload"; filename="milk.md"
        Content-Type: application/octet-stream

        |鍝佺墝|铔嬬櫧璐ㄥ惈閲忥紙g/100mL锛墊鑴傝偑鍚噺锛坓/100mL锛墊瑙勬牸|浠烽挶锛堝厓锛墊杩愯垂锛堝厓锛墊鎶樺悎姣忕洅鍗曚环锛堝厓/鐩掞級|鎶樺悎姣?00mL鍗曚环锛堝厓/100mL锛墊
        |---|---|---|---|---|---|---|---|
        |涓婅川 EULAUD娆у矚|3.3|3.5|200mL x 30|69|0|2.3|1.15|
        |Arla鐖辨皬鏅ㄦ洣|3.4|3.6|200ml x 24|49|6|2.29|1.15|
        |绾介害绂忥紙Meadow fresh锛墊3.5|3.5|250ml x 24|64.9|6|2.95|1.18|
        |缁寸函锛坴italife锛墊3.32|1.52|250ml x 24|49.9 + 6.65锛堢◣璐癸級|6|2.61|1.04|
        ------WebKitFormBoundaryRdlJBEfBAruajkfg
        Content-Disposition: form-data; name="submit"

        鎻愪氦
        ------WebKitFormBoundaryRdlJBEfBAruajkfg--
        </pre>

      6. 对照《文件上传原理思想》图,可以很清晰的找到上传的文件内容在 http 请求体中的位置。

2、利用 Commons-fileupload 组件实现文件上传

  • 为方便用户处理文件上传数据,Apache 开源组织提供了一个用来处理表单文件上传的一个开源组件( Commons-fileupload ),该组件性能优异,并且其 API 使用极其简单,可以让开发人员轻松实现 web 文件上传功能,因此在 web 开发中实现文件上传功能,通常使用 Commons-fileupload 组件实现。

  • 使用 Commons-fileupload 组件实现文件上传,需要导入该组件相应的支撑 jar 包: Commons-fileupload 和 commons-io。commons-io 不属于文件上传组件的开发 jar 文件,但 Commons-fileupload 组件从1.1 版本开始,它工作时需要 commons-io 包的支持。

    Commons-fileupload 官网介绍&文档&下载:http://commons.apache.org/proper/commons-fileupload/

    注意:下载 jar 包时,请仔细阅读版本号与所需 JDK 版本之间的关系,比如 commons-io-2.5 最低要求 JDK 1.6+,这个 JDK 1.6+ 对应 JavaEE 6.0,也就是说在 MyEclipse 10 新建工程时,需要选择 JavaEE 6.0。

  • 导入 jar 包后,在处理表单的 Servlet 中应该按如下步骤使用 API,四个步骤很重要

    1. 创建 DiskFileItemFactory 对象,设置缓冲区大小和临时文件目录。

    2. 使用 DiskFileItemFactory 对象创建 ServletFileUpload 对象,并设置上传文件的大小限制。

    3. 调用 ServletFileUpload.parseRequest() 方法解析 request 对象,得到一个保存了所有上传内容的 List 对象。

    4. 对 List 进行遍历,每遍历一个 FileItem 对象,调用其 FileItem.isFormField() 方法判断该对象是否是上传文件对象,该方法的返回值具体含义为:

      • True 为普通表单字段,则调用 getFieldName() 获得 name 属性、getString() 方法获得 value 属性。

      • False 为上传文件,则调用 getInputStream() 方法得到文件内容、getName() 方法获得文件名。

        注意:对于得到的文件名,不同的浏览器有不同的结果,比如使用 IE 6 得到的文件名包含上传文件所在的原来客户端的路径,通用解决办法如下:

          String filename = fileItem.getName(); // 文件名
        
              // 解决老版本浏览器IE6 文件路径存在问题
              if (filename.contains("\\")) {
                  filename = filename.substring(filename.lastIndexOf("\\")+ 1);
              }
        

3、核心API——DiskFileItemFactory

  • 在前文已经描述了处理表单的 Servlet 应该如何编写,其步骤的第一点就是要先创建一个 DiskFileItemFactory 对象,接下来就详细讲讲 DiskFileItemFactory 该怎么使用,首先创建对象:

      DiskFileItemFactory factory = new DiskFileItemFactory();
    
  • DiskFileItemFactory 是创建 FileItem 对象的工厂,这个工厂类常用方法:

    • public DiskFileItemFactory(int sizeThreshold, java.io.File repository) :构造函数

    • public void setSizeThreshold(int sizeThreshold) :
      设置内存缓冲区的大小,默认值为 10K。当上传文件大于缓冲区大小时, fileupload 组件将使用临时文件缓存上传文件。

        // 设置缓冲区大小和临时目录
        factory.setSizeThreshold(1024 * 1024 * 8);// 8M 临时缓冲区(上传文件不大于8M
        // 不会产生临时文件)
      
    • public void setRepository(java.io.File repository) :
      指定临时文件目录,默认值为 System.getProperty("java.io.tmpdir").

        File repository = new File(getServletContext().getRealPath(
                "/WEB-INF/tmp"));
        factory.setRepository(repository);// 当上传文件超过8M 会在临时目录中产生临时文件,临时文件与源文件内容相同。
      

    原理:上传文件优先保存内存缓冲区,当内存缓存区不够用,在硬盘上产生临时文件,临时文件保存指定临时文件目录中。
    临时文件耗费空间资源,应该在上传结束之后把 fileItem 删除。具体做法为:先关闭 FileItem 的输入流,再调用 FileItem.delete() 方法删除文件。

4、核心API——ServletFileUpload

  • 在步骤二中,通过 DiskFileItemFactory 对象创建 ServletFileUpload 对象。而ServletFileUpload 负责处理上传的文件数据,并将表单中每个输入项封装成一个 FileItem 对象中。

  • 常用方法:

    • 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) :
      设置编码格式

      乱码解决问题一:默认情况下,上传文件名如果含有中文,则上传后的文件名会变成乱码,为了解决,必须在步骤二中调用该方法,并把编码格式设置 " utf-8"。

        ServletFileUpload.upload.setHeaderEncoding("utf-8");
      
    • setProgressListener(ProgressListener pListener) :
      实时监听文件上传状态,典型应用是文件上传的进度条(重难点)

5、核心API——FileItem

  • 在步骤二中,ServletFileUpload 的作用是将表单的每个输入项封装成一个 FileItem 对象,这里就着重讲讲 FileItem 要怎么用。

  • 常用方法:

    • isFormField() :是否为文件上传域,true不是文件上传,false是文件上传

    • (对于非文件上传域)getFieldName :获得表单的 name 属性

    • (对于非文件上传域)getString :获得表单中该输入项的值(value)

      乱码解决问题二:文件上传表单中request.setCharacterEncoding 不能使用,同样 request.getParameter 不能使用,所以如果表单某个输入框中有中文,要解决乱码问题,可以传入一个编码类型的参数:

        String value = fileItem.getString("utf-8"); // 得到某个表单输入项的值
      
    • (对于文件上传域)getName :获得文件名

    • (对于文件上传域)getInputStream : 获得文件内容

6、拓展——使用 JavaScript 生成多个动态上传输入项

  • 功能需求:每次动态增加一个文件上传输入框,都把它和删除按纽放置在一个单独的 div 中,并对删除按纽的 onclick 事件进行响应,使之删除删除按纽所在的 div 。

  • 代码:

      this.parentNode.parentNode.removeChild(this.parentNode);
    
      <%@ 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>Insert title here</title>
      <script type="text/javascript">
          function addAttach() {
              // 在div中添加 文件上传框
              var attachments = document.getElementById("attachments");
              attachments.innerHTML += "<div><input type='file' /><input type='button' value='删除' onclick='delAttach(this);' /></div>";
          }
      
          function delAttach(btn) {
              // 传入参数 当前点击按钮
              //alert(btn.nodeName);
      
              // 先获得删除 div
              var wantDelDiv = btn.parentNode;
      
              // 用要删除div找到父亲,杀死
              wantDelDiv.parentNode.removeChild(wantDelDiv);
      
          }
      </script>
      </head>
      <body>
          <!-- JS 编写动态文件上传框 -->
          <input type="button" value="添加附件" onclick="addAttach();" />
          <div id="attachments"></div>
      </body>
      </html>
    
  • 效果:

7、文件上传的细节处理问题

  • 乱码问题

    • 上传文件名乱码:servletFileupload.setHeaderEncoding("utf-8") —— 乱码解决问题一

    • 表单普通项乱码:fileItem.getString("utf-8") —— 乱码解决问题二

  • 临时文件删除

    必须先关闭 FileItem 的输入流,再调用 FileItem.delete ()方法 删除临时文件。

  • 上传文件保存目录

    首先应该明白一个概念:web 工程中 WEB-INF 在浏览器端不允许通过 URL 直接访问。

    • WEB-INF内 :必须通过服务器端程序去访问,Servlet ---- getRealPath(/WEB-INF) ---------------- 需要权限,需要身份认证

    • WEB-INF外 :浏览器直接通过URL访问 ------------------ 任何人都可以访问

    • 思考:假设有个电影会员点播网站,可以付费在线看电影。那么上传电影应该放到哪里? 答案:WEB-INF 里面,客户端不能直接访问。再分析:淘宝里的商家上传商品图片,商品图片应该放在哪里?答案: WEB-INF 外面,客户端可以直接访问。

  • 文件覆盖

    假设客户端上传的所有文件都放到同一个目录中,当文件名重名时,会发生文件覆盖,如何解决?答案:文件名唯一。

      // 保证上传文件名唯一
      filename = UUID.randomUUID().toString() + filename;
    
  • 多目录分散

    当上传文件很多时,如果不对文件分散到不同目录去,那就会集中在同一个目录中,这样访问某个文件需要很长时间,甚至不响应,查找困难,所以应该采用目录分散算法,有如下几种目录分散算法:

    • 按时间 —— 比如一天一个目录

    • 按用户 —— 比如淘宝某个商家上传的所有商品图片都放在一个单独目录

    • 每个目录存放固定数量文件 —— 每个目录存放1000个文件,每次上传文件时判断目录中文件是否超过1000,若超过则新建一个目录,保存在新目录中

    • 哈希目录分散算法 —— 对于一个对象,它会有一个 32 位的 hashcode ,这个 hashcode 通过一定的哈希算法生成,允许重复。把 32 位的 hashcode 分成 4*8,每次与 1111 进行“位与”运算,得到一个 4 位的值(0~15),然后右移 4 位,继续“位与”运算,每右移一次,目录就会增加一个层级,可根据不同的业务要求决定具体的目录层级数目,对于小型项目而言,2 级目录即可满足需求(总共有(2^4)^2=256个文件夹)。具体如图所示:

      对于图中数据来说,它的 1 级目录号为 12,它 2 级目录号为 14,故这个文件位于12号文件夹的14号文件夹里面。

      • 可编写一个工具类:

          public class UploadUtils {
              // 获得随机目录
              public static String generateRandomPath(String fileName) {
                  int hashcode = fileName.hashCode();
                  int d1 = hashcode & 0xf;
                  int d2 = (hashcode >> 4) & 0xf;
                  return "/" + d1 + "/" + d2;
              }
          }
        
      • 然后在 Servlet 中这样调用:

          // 生成随机目录
          String randomPath = UploadUtils
                  .generateRandomPath(filename);// 生成目录不一定存在 ---创建
          File path = new File(getServletContext().getRealPath(
                          "/WEB-INF/upload" + randomPath));
          path.mkdirs();
        

8、文件上传进度监听器——ProgressListener

  • 官方API : public interface ProgressListener -- The ProgressListener may be used to display a progress bar or do stuff like that.

  • 最重要的 update 方法:

      void update(long pBytesRead,
          long pContentLength,
          int pItems)
    

-- Updates the listeners status information.

Parameters:

- pBytesRead - The total number of bytes, which have been read so far.

- pContentLength - The total number of bytes, which are being read. May be -1, if this number is unknown.

- pItems - The number of the field, which is currently being read. (0 = no item so far, 1 = first item is being read, ...)
  • 使用代码:

          // 设置文件上传监听器
          ProgressListener listener = new ProgressListener() {
              
              // 在文件上传过程中,文件上传程序,会自动调用update方法,而且不只一次调用,通过该方法,获得文件上传进度信息
              // pBytesRead 已经上传字节数量
              // pContentLength 上传文件总大小
              // pItems 表单项中第几项
              public void update(long pBytesRead, long pContentLength,
                      int pItems) {
                  System.out.println("上传文件总大小:" + pContentLength + ",已经上传大小:"
                          + pBytesRead + ", form第几项:" + pItems);
              }
          };
    
  • 为了更友好的用户体验,页面应该实时显示上传的速度、剩余时间甚至用进度条美化。接下来就探讨一下如何实现,稍微思考下可以得到下面两个公式,其中已经使用时间=现在时间-起始时间

    • 平均传输速率 = 已经上传大小/已经使用时间;

    • 剩余时间 = 剩余大小/平均传输速率;

    • 于是在步骤二中,这样编写代码:

        // 步骤二 获得解析器
        ServletFileUpload upload = new ServletFileUpload(factory);
        final long start = System.currentTimeMillis();
      
        // 设置文件上传监听器
        ProgressListener listener = new ProgressListener() {
            // 在文件上传过程中,文件上传程序,会自动调用update方法,而且不只一次调用,通过该方法,获得文件上传进度信息
            // pBytesRead 已经上传字节数量
            // pContentLength 上传文件总大小
            // pItems 表单项中第几项
            public void update(long pBytesRead, long pContentLength,
                    int pItems) {
                System.out.println("上传文件总大小:" + pContentLength + ",已经上传大小:"
                        + pBytesRead + ", form第几项:" + pItems);
                // 通过运算获得其它必要信息:传输速度、剩余时间
                long currentTime = System.currentTimeMillis();
                // 已经使用时间
                long hasUseTime = currentTime - start;
                // 速率
                // 字节/毫秒 = x/1024*1000 KB/S
                long speed = pBytesRead / hasUseTime;
                // 剩余时间
                long restTime = (pContentLength - pBytesRead) / speed;// 毫秒
                System.out.println("传输速度:" + speed + "字节每毫秒,剩余时间"
                        + restTime + "毫秒");
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        upload.setProgressListener(listener); // 注册监听器
      
    • 当然,这里考虑的是平均传输速度,也可以改为瞬时传输速度:每过一秒,计算在这一秒中上传的字节。

9、文件下载概述

  • web 应用中实现文件下载的两种方式:

    • 方式一:超链接直接指向下载资源

      这时将使用 DefaultServlet ,它会将资源返回,如果浏览器对该资源不支持直接打开(比如说 Excel 文档),则会询问用户是否下载,如果浏览器识别下载文件格式,则会自动打开(比如说图片)

    • 方式二:编写程序实现下载,这时这时需设置两个响应头,需要要符合 Mime 协议

      设置 Content-Type 的值为:下载文件所对应 MIME 类型、 web 服务器希望浏览器不直接处理相应的实体内容,而是由用户选择将相应的实体内容保存到一个文件中,这就需要设置 Content-Disposition (以附件形式传输),在设置 Content-Dispostion 之前一定要指定 Content-Type。

      注意:ServletContext.getMimeType(file) 可以得到下载资源所对应的 Mime 类型,你也可以在 tomcat/conf/web.xml 文件中查询各种 MIME 类型。

        // 获得客户端提交file 参数
        String file = request.getParameter("file");
        // 下载文件,从服务器端读取文件,将文件内容写回客户端
        String serverFilePath = getServletContext().getRealPath(
                "/download/" + file);
        
        // 设置响应头,等效于于 response.setHeader("Content-Type",getServletContext().getMimeType(file));
        response.setContentType(getServletContext().getMimeType(file));// 根据文件扩展名获得MIME类型
      
        response.setHeader("Content-Disposition", "attachment;filename=" + file);// 以附件下载
      
    • 方式二的代码实现:

      • 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>Insert title here</title>
          </head>
          <body>
          <h1>使用链接方式实现资源下载</h1>
          <a href="/day20/download/1.jpg">1.jpg</a><br/>
          <a href="/day20/download/2.xls">2.xls</a><br/>
          <a href="/day20/download/3.rar">3.rar</a><br/>
          <a href="/day20/download/4.txt">4.txt</a><br/>
          <h1>通过Servlet完成资源下载</h1>
          <a href="/day20/downloadFile?file=1.jpg">1.jpg</a><br/>
          <a href="/day20/downloadFile?file=2.xls">2.xls</a><br/>
          <a href="/day20/downloadFile?file=3.rar">3.rar</a><br/>
          <a href="/day20/downloadFile?file=4.txt">4.txt</a><br/>
          </body>
          </html>
        
      • Servlet :

          public class DownloadFileServlet extends HttpServlet {
              public void doGet(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
                  // 获得客户端提交file 参数
                  String file = request.getParameter("file");
                  // 下载文件,从服务器端读取文件,将文件内容写回客户端
                  String serverFilePath = getServletContext().getRealPath(
                          "/download/" + file);
          
                  // 设置响应头
                  response.setContentType(getServletContext().getMimeType(file));// 根据文件扩展名获得MIME类型
                  // 等级于 response.setHeader("Content-Type",xxx);
                  response
                          .setHeader("Content-Disposition", "attachment;filename=" + file);// 以附件下载
          
                  InputStream in = new BufferedInputStream(new FileInputStream(
                          serverFilePath));
                  // 需要浏览器输出流
                  OutputStream out = response.getOutputStream();
                  int temp;
                  while ((temp = in.read()) != -1) {
                      out.write(temp);
                  }
                  out.close();
                  in.close();
              }
          
              public void doPost(HttpServletRequest request, HttpServletResponse response)
                      throws ServletException, IOException {
                  doGet(request, response);
              }
          }
        

10、深度优先搜索与广度优先搜索

  • 目录就是树形结构,根目录代表根节点,每个子节点代表一个子目录,直到端节点,端节点就代表各种文件。在遍历树形结构时,有两种主要方法,深度优先搜索(depth first search)与广度优先搜索(breadth first search)。其区别如图所示:

  • 假设现在有个需求:将某目录中所有 mp3 文件显示在浏览器上,并提供下载功能,而这些 mp3 文件可能在不同的子目录下,这时要怎么做呢?

  • 若想实现广度优先搜索,这时可利用 LinkedList 来解决问题,jsp 文件如下:

      <%@ page language="java" contentType="text/html; charset=UTF-8"
          pageEncoding="UTF-8"%>
      <%@page import="java.util.LinkedList"%>
      <%@page import="java.io.File"%>
      <%@page import="java.net.URLEncoder"%>
      <!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>Insert title here</title>
      </head>
      <body>
      <h1>文件下载列表</h1>
      <!-- 将D:\CloudMusic 中所有音乐文件,显示列表,允许用户下载 -->
      <%
          // 遍历指定目录 --- 非递归广度
          File root = new File("D:\\CloudMusic");
          LinkedList<File> list = new LinkedList<File>();// 存储
          list.add(root);// 集合中存在一个目录
          
          while(!list.isEmpty()){
              // 集合不为空
              File currentDir = list.removeFirst();// 返回目录对象
              File[] files = currentDir.listFiles();// 获得目录下所有文件
              for(File f : files){
                  if(f.isDirectory()){
                      // 将未遍历目录 加入集合
                      list.add(f);
                  }else{
                      // 是一个文件
                      // get 、post 提交中文,使用URL编码
                      String args = URLEncoder.encode(f.getCanonicalPath(),"utf-8");// 这个本来是浏览器默认动作,只是手动执行
                      out.println("<a href='/day20/downloadMusic?path="+args+"'>"+f.getName()+"</a><br/>");
                  }
              }
          }
      %>
      </body>
      </html>
    

11、文件下载的细节处理问题

  • 为了获得服务器的资源,应该提供资源的绝对磁盘路径,这里有两种方式:

    • File.getAbsolutePath() --返回此抽象路径名的绝对路径名字符串,不唯一

    • FIle.getCanonicalPath() -- 返回返回此抽象路径名的规范路径名字符串,唯一

    • 通过比较可以发现,下面一种写法更加符合规范,推荐使用。

  • 请求的乱码问题

    在深度优先搜索的 jsp 示例中,注意如下的写法:

      String args = URLEncoder.encode(f.getCanonicalPath(),"utf-8");
    

    意义:get、post 方式访问,会使用 URL 编码,而目录有可能含有中文,如果不进行 utf-8 编码,则可能会根据不同的浏览器而报错。

  • 响应的乱码问题

    在文件下载时,点击链接会弹出下载框,如果不进行处理,则资源名存在乱码问题,在本文的《9、文件下载概述》中有如下代码:

      // 设置响应头
      response.setContentType(getServletContext().getMimeType(file));// 根据文件扩展名获得MIME类型
      // 等级于 response.setHeader("Content-Type",xxx);
      response
              .setHeader("Content-Disposition", "attachment;filename=" + file);// 以附件下载
    

    注意:不同的浏览器对于响应的编码方式有所不同,比如 IE 采用 URL 编码,火狐采用该 base64 编码。

    再思考,如何根据不同的浏览器选择合适的编码方式呢?很显然,只要我们知道了当前客户端是什么浏览器就可以 if - else 判断,然后执行不同的编码方式。

    答案:在请求的头信息中,两个浏览器在 User-Agent 中有不同的值,IE 浏览器多了 MSIE 这个信息,IE 和火狐都有 Mozilla 这个信息,所以可以做如下判断(这段代码可重用):

      String agent = request.getHeader("User-Agent");
      if (agent.contains("MSIE")) {
          // IE 浏览器 采用URL编码
          filename = URLEncoder.encode(filename, "utf-8");
          response.setHeader("Content-Disposition", "attachment;filename="
                  + filename);
      } else if (agent.contains("Mozilla")) {
          // 火狐浏览器 采用Base64编码
          // filename = MimeUtility.encodeText(filename);// 调用这个方法时,如果参数为全英文,则不编码,所以不能简单调用该方法,应该如下手动编码
          BASE64Encoder base64Encoder = new BASE64Encoder();
          filename = "=?UTF-8?B?"
                  + new String(base64Encoder.encode(filename
                          .getBytes("UTF-8"))) + "?=";
    
          response.setHeader("Content-Disposition", "attachment;filename="
                  + filename);
      } else {
          // 默认 不编码
          response.setHeader("Content-Disposition", "attachment;filename="
                  + filename);
      }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容