1. 程式人生 > >Java Web 之檔案上傳與下載

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 協議對上傳的檔案進行描述,以方便接收方對上傳資料進行解析和處理。

      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. 點選提交按鈕,在控制檯可以看到如下輸出內容:

         ------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--
      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 包的支援。

    注意:下載 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);
      }