JSP 技巧 - 檔案下載
作者:葉愚
日期:Feb-11-2004摘要本文介紹如何撰寫一個檔案下載的 JSP 程式,以及一些注意事項。最後將範例程式改寫成 JSP 搭配JavaBean 的方式,以提高程式碼的重複使用性。適用:J2EE 1.3
--------------------------------------------------------------------------------簡介
檔案下載在一般的網頁應用程式很常見到,也許您會問:「不就提供個超連結麼?有啥好寫的?」一般的情形確實如此,例如:某個網頁上面提供了一個  zip 檔的超連結,當使用者在點擊這個超連結時,瀏覽器就會出現「另存新檔」的視窗,讓使用者將檔案下載到指定的目錄存放;可是如果下載的檔案類型是 txt, pdf, doc,....等等,瀏覽器就會直接開啟這些檔案,而不會出現存檔視窗。在某些情況下,你可能只想讓使用者下載檔案,不要直接在瀏覽器中開啟文件,本文所要介紹的技巧,便是讓瀏覽器不管什麼檔案類型,都會詢問使用者是否存檔。使用這種方式提供檔案下載有兩個特別的用處:(1) 可以只讓授權的使用者才能下載特定的檔案,而不是任何知道檔案的 URL 的人就能下載,也就是說,欲傳送的檔案實際上可以不存放在網站的虛擬目錄中,以避免任意使用者非法存取這些檔案;(2) 要提供給前端下載的檔案內容可以是靜態的實體檔案,也可以是任何動態產生的內容,例如從資料庫中取出的資料。範例程式
這個技巧說穿了其實很簡單,主要就是透過更改回應的 HTTP header 資訊,來告訴瀏覽器:請不要直接開啟這份文件。但是其中仍有幾個值得一提的地方,例如經常被問到的下載後的檔案內容中文變成亂碼的問題,以及錯誤發生時如何處理等等。我們先看一下這個範例的完整 JSP 程式碼,後面再逐一說明:01: <%@ page 
02:   language="java"
03:   contentType="text/html; charset=big5"
04:   pageEncoding="BIG5"
05:   import="java.util.*, java.io.*, java.net.URLEncoder" 
06: %><%
07:   String src_fname = "c:\\temp\\測試.txt"; 
08:   String dst_fname = URLEncoder.encode("測試.txt");
09: 
10:   request.setCharacterEncoding("big5");  
11:   
12:   response.setContentType("application/octet-stream; charset=iso-8859-1");
13:   response.setHeader("Content-disposition", "attachment; filename=\"" + dst_fname + "\"");
14:   
15:   FileInputStream fis = null; 
16:   int byteRead; 
17:   
18:   try {
19:     fis = new FileInputStream(src_fname);
20:     while ((byteRead = fis.read()) != -1) {
21:       out.write(byteRead);
22:     }
23:     out.flush();
24:   } 
25:   catch (Exception e) {
26:     out.clearBuffer();
27:     response.setContentType("text/html; charset=big5");
28:     response.setHeader("Content-disposition", "inline");
29:     out.println("<HTML><BODY><P>");
30:     out.println(e.toString());
31:     out.println("</P></BODY></HTML>");    
32:   } 
33:   if (fis != null) {
34:     fis.close();
35:   }
36:   return; // 避免下面多按了 Enter 鍵而輸出多餘的換行字元.
37: %>
 在撰寫這個程式時,首先要特別留意:不要輸出多餘的換行字元。如果你不了解其中原委,很容易就會出錯。例如,如果你把程式中的第 1~6 行改成這麼寫:01: <%@ page 
02:   language="java"
03:   contentType="text/html; charset=big5"
04:   pageEncoding="BIG5"%>
05: <% import="java.util.*, java.io.*, java.net.URLEncoder" %>
06: <%
....(以下略)
 那麼使用者下載的檔案前面至少會平白多出兩行,因為這個檔案下載的程式,實際上跟一般的 JSP 網頁沒太大差別,所有的網頁內容都會傳送至前端的瀏覽器,只是我們透過第 13 行更改 HTTP header 的方式,讓這些內容不會直接顯示在瀏覽器中。所以,在 JSP 中的任何多餘的換行,都會導致最後下載的檔案和原始的檔案內容不符,這也是第 36 行要加上 return 敘述的原因,這樣可以防止你不小心在檔尾多按了 Enter 鍵(編譯會不通過)。第 7~8 行指定了來源檔名和預設的目的檔名,來源檔名要包含完整的絕對路徑名稱,目的檔名則不需要。實際應用時,檔案名稱可以透過 HTTP request 參數傳遞的方式,由外界指定,因此第 10 行將 request 的字元編碼設定為 "BIG5",以便未來傳遞參數時能夠正確處理中文字。另外值得一提的,是第 8 行使用了 URLEncoder.encode() 來將目的檔名重新編碼,如果不這麼做,用戶端會無法正確顯示中文檔名。由於筆者的開發環境僅支援到 J2EE 1.3,到了 1.4 版時,這個只帶一個參數的 URLEncoder.encode() 方法已經被標示為過時(deprecated),並且增加了一個同名函式,這個新加入的函式可以讓你明白指定字元編碼的方式,細節的部分請讀者自行參考 JDK 1.4 的說明文件。第 12 行指定了輸出文件的 content-type。雖然前面已經用 page 指示項(directive)設定過 content-type,這裡還是再強制設定一次,其中 "application/octet-stream" 表示這份文件的型態是「未解譯的二進位碼」;而 charset 要使用 "iso-8859-1",這樣下載的檔案不管是純文字檔,還是二進位檔,都沒有問題,筆者試過如果 charset 設成 "BIG5",純文字檔可正確傳送,但是二進位檔的內容會不正確。第 13 行設定回應的 HTTP header,其中傳入 setHeader() 的第二個字串參數裡面的 "attachment; filename=,,,," 就是讓瀏覽器不會直接顯示檔案內容的關鍵。如果把第二個參數改成 "inline",瀏覽器就會直接顯示檔案內容了。第 15~24 行將檔案內容逐一讀取出來,然後輸出到網頁的內容。第 25~32 行則是當異常發生時,先清除目前輸出的網頁內容,然後重新設定網頁的型態是一般的 HTML 文件,並且在瀏覽器中顯示錯誤訊息。執行此範例程式時,要先將 "測試.txt" 這個檔案放在 c:\temp\ 目錄下,如果程式的檔名叫做 download.jsp,你可以直接在瀏覽器中輸入網址來觀看執行結果,例如: "http://localhost:8080/MyDemo/download.jsp"。提昇重複使用性
這個程式大部分都是 Java 程式碼,因此可以考慮將處理檔案下載的 Java 程式碼寫成一個 JavaBean 的方法,再由 JSP 去呼叫這個方法,呼叫時必須將 request、response、來源檔名、和預設的目的檔名等參數傳入。假設我們把這個方法放到 com.darkside.JspUtil 這個類別裡,它看起可能會是這樣:package com.darkside;import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*
import javax.servlet.jsp.JspWriter;
import java.net.URLEncoder;public final class JspUtil {  /**
   * 提供 JSP/servlet 檔案下載的功能。
   * @param req 由 JSP/servlet 傳入的 Request 物件。
   * @param resp 由 JSP/servlet 傳入的 Response 物件。
   * @param src_fname 來源檔名,包含完整路徑名。
   * @param dst_fname 目的檔名,不用路徑名稱。
   * @throws ServletException
   * @throws IOException
   */
  public static void download(
    HttpServletRequest req,
    HttpServletResponse resp,
    String src_fname,
    String dst_fname)
    throws ServletException, IOException {    // ....略 
  }
}
 其中省略的程式碼幾乎是從原來的 download.jsp 搬過來,只要稍加修改即可,故不列出。修改後的 download.jsp 如下:<%@ page 
  language="java"
  contentType="text/html; charset=big5"
  pageEncoding="big5"
  import="com.darkside.JspUtil"
%><%
  try {
    JspUtil.download(request, response, "c:\\測試.txt", "測試.txt");
  } 
  catch (Exception e) { // 若發生錯誤,就改由瀏覽器顯示錯誤訊息
    // ....略
  }
%>
 這樣做除了 JSP 程式碼變得比較簡潔,而且也提高了程式碼的重複使用性,以後如果別的 JSP 也需要使用檔案下載功能,只要呼叫 JspUtil.download() 即可。結語
本文介紹了 JSP 檔案下載的技巧,並提供一個現成可用的範例程式,文中詳細解說了程式碼的每個部分,希望讀者能了解每一行程式碼的作用,以便日後能夠修改以符合個人的需求,並且減少嘗試錯誤的時間。此外,筆者也介紹了如何將 JSP 程式中可以重複使用的部分抽離出來,寫成 JavaBean 的方法,以盡量提高程式碼的重複使用性。