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 開發中的檔案上傳功能,需完成如下兩步操作:
-
在 jsp 頁面中新增上傳輸入項
-
在servlet中讀取上傳檔案的資料,並儲存到伺服器硬碟中
-
第一步:
-
如何在 jsp 頁面中新增上傳輸入項?
-
<input type="file">
標籤用於在 jsp 頁面中新增檔案上傳輸入項,設定檔案上傳輸入項時須注意:-
必須要設定 input 輸入項的 name 屬性,否則瀏覽器將不會發送上傳檔案的資料。
-
必須把 form 的 enctype 屬性值設為 multipart/form-data 。其實 form 表單在你不寫 enctype 屬性時,也預設為其添加了 enctype 屬性值,預設值是
enctype="application/x- www-form-urlencoded"
設定該值後,瀏覽器在上傳檔案時,將把檔案資料附帶在 http 請求訊息體中,並使用 MIME 協議對上傳的檔案進行描述,以方便接收方對上傳資料進行解析和處理。 -
表單的提交方式必須是post,因為上傳檔案可能較大。
get:以【明文】方式,通過URL提交資料,資料在URL中可以看到。提交資料最多不超過【2KB】。安全性較低,但效率比post方式高。適合提交資料量不大,且安全要求不高的資料:比如:搜尋、查詢等功能。
post:將使用者提交的資訊封裝在HTML HEADER內,資料在URL中【不能看到】適合提交資料量大,安全性高的使用者資訊。如:註冊、修改、上傳等功能。
區別:
-
post隱式提交,get顯式提交。
-
post安全,get不安全。
-
get提交資料的長度有限(255字元之內),post無限。
-
-
-
示例:
<form action="xx.action" method="post" enctype="multipart/form-data"> </form>
-
第二步
-
如何在 Servlet 中讀取檔案上傳資料,並儲存到本地硬碟中?
-
Request 物件提供了一個 getInputStream 方法,通過這個方法可以讀取到客戶端提交過來的資料(具體來說是 http 的請求體 entity)。但由於使用者可能會同時上傳多個檔案,在 Servlet 端程式設計直接讀取上傳資料,並分別解析出相應的檔案資料是一項非常麻煩的工作。
-
具體工作:假設我們獲取了 http 的請求體,如何得到上傳的檔案呢?如圖:左邊是 http 的請求體,右邊是步驟:
檔案上傳原理思想 -
我們來實際操作一下到底是怎麼工作的:
-
首先我們先寫個簡單的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>
-
然後開啟 Tomcat ,填寫使用者名稱,選擇上傳一個 md 檔案,如下圖所示:
-
md 檔案內容如圖所示:
-
在 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); } }
-
點選提交按鈕,在控制檯可以看到如下輸出內容:
------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--
-
對照《檔案上傳原理思想》圖,可以很清晰的找到上傳的檔案內容在 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,四個步驟很重要:
-
建立 DiskFileItemFactory 物件,設定緩衝區大小和臨時檔案目錄。
-
使用 DiskFileItemFactory 物件建立 ServletFileUpload 物件,並設定上傳檔案的大小限制。
-
呼叫 ServletFileUpload.parseRequest() 方法解析 request 物件,得到一個儲存了所有上傳內容的 List 物件。
-
對 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); }