Wednesday, November 9, 2011

使用HttpServlet做檔案下載


檔案下載是網站程式中常見的需求。也許只是一般檔案要下載,或許是動態產生的圖檔。有時候把檔案直接放在公開的網站目錄中直接給他的連結給人下載就可以了。不過當成是稍為複雜時,這些檔案需要與網站部屬目錄分開處理,或者動態產生的內容不經過寫到檔案直接用stream的方式傳出來。

以下是一個共用的Servlet,提供最基本的檔案下載功能。他設為Abstract不能直接使用,必須另外寫一個繼承它的Servlet類別。他最主要提供了sendDownloadFile的兩個多載的方法。一個提供了直接是用stream傳輸資料的放方,另一個只須給電腦中檔案的完整路徑就可以傳輸該檔案了。

所以,很簡單,繼承FileDownloadServlet,在doGet或doPost中寫自己程式的邏輯,如找出要傳輸的檔案或檢查使用者權限等。然後把檔案完整路徑或產生的stream傳給sendDownloadFile好了。

在沒有特別的Framework支援,或在Portal下。這個Servlet可以簡化很多工作。

這個是FileDownloadServlet::
   1: import java.io.BufferedInputStream;
   2: import java.io.File;
   3: import java.io.FileInputStream;
   4: import java.io.FileNotFoundException;
   5: import java.io.IOException;
   6: import java.io.InputStream;
   7: import java.io.OutputStream;
   8:  
   9: import javax.servlet.http.HttpServlet;
  10: import javax.servlet.http.HttpServletResponse;
  11: /**
  12:  * 檔案下載共用元件
  13:  */
  14: public abstract class FileDownloadServlet extends HttpServlet {
  15:     private static final long serialVersionUID = 716097697136220545L;
  16:  
  17:     private static final int DEFAULT_STREAM_BUFFER_SIZE  = 1024;
  18:     private static final String CONTENT_DISPOSITION_FORMAT = "%s; filename=\"%s\"";
  19:         
  20:     private String contentDisposition = "attachment";
  21:  
  22:     protected int getStreamBufferSize() {
  23:         return DEFAULT_STREAM_BUFFER_SIZE;
  24:     }
  25:  
  26:     //叫瀏覽器直接開在瀏覽器裡
  27:     protected void setContentDispositionInline() {
  28:         contentDisposition = "inline";
  29:     }
  30:  
  31:     //存檔就好
  32:     protected void setContentDispositionAttachment() {
  33:         contentDisposition = "attachment";
  34:     }
  35:     
  36:     protected void sendDownloadFile(HttpServletResponse response, InputStream in, int contentLength, String contentType, String saveAsFileName) throws IOException {
  37:         response.setContentType(contentType);
  38:         response.setContentLength(contentLength);
  39:         response.setHeader("Content-Disposition", String.format(CONTENT_DISPOSITION_FORMAT, contentDisposition, saveAsFileName));
  40:  
  41:         byte[] buf = new byte[getStreamBufferSize()];
  42:         BufferedInputStream from = new BufferedInputStream(in);
  43:         OutputStream to = response.getOutputStream();
  44:         
  45:         int length = from.read(buf);
  46:         while(length > 0) {
  47:             to.write(buf, 0, length);
  48:             length = from.read(buf);
  49:         }
  50:         
  51:         to.flush();
  52:         to.close();
  53:     }
  54:     
  55:     protected void sendDownloadFile(HttpServletResponse response, String fullFilePath, String saveAsFileName) throws FileNotFoundException, IOException {
  56:         File file = new File(fullFilePath);
  57:         String contentType = getServletConfig().getServletContext().getMimeType(fullFilePath);
  58:         InputStream in = new FileInputStream(file);
  59:         sendDownloadFile(response, in, (int)file.length(), contentType, saveAsFileName);
  60:         in.close();
  61:     }
  62:  
  63: }


讓伺服器能夠與瀏覽器傳輸檔案的方式是指定contentType。一般的網頁contentType是text/html,瀏覽器認得此資料類別直接將它顯示出來,就是我們每天看到的網頁了。檔案傳輸只是將contentType改了一下,剩下的就是將要傳送的資料以串流方式傳出。

在使用完整路徑傳送檔案的sendDownloadFile方法中,它呼叫了getServletConfig().getServletContext().getMimeType(fileFullPath),用意是請執行程式的容器(Tomcat或其他Java EE server)依據傳入的檔名給我們正確的contentType。

sendDownloadFile的saveAsFileName是告訴瀏覽器存檔時預設的檔名,設在contentDisposition。

可以覆寫getStreamBufferSize回傳在傳輸檔案時使用buffer的大小,沒有覆寫的話會使用DEFAULT_STREAM_BUFFER_SIZE,也就是1024。

呼叫setContentDispositionInline跟setContentDispositionAttachment分別代表告訴瀏覽器要直接顯示或存檔。不過要不要照著做還是要看瀏覽器乖不乖聽不聽話。


接下來是一個繼承FileDownloadServlet的servlet,這隻servlet主要在接網址列傳過來叫做id的參數
 使用的路徑可能是 http://localhost/AttachmentDownload?id=120
他會去資料庫查詢該筆資料的其它欄位資料:如檔名和檔案實際存放的路徑,

   1: import java.io.File;
   2: import java.io.IOException;
   3:  
   4: import javax.servlet.ServletConfig;
   5: import javax.servlet.ServletException;
   6: import javax.servlet.http.HttpServlet;
   7: import javax.servlet.http.HttpServletRequest;
   8: import javax.servlet.http.HttpServletResponse;
   9:  
  10: import org.springframework.beans.factory.annotation.Autowired;
  11: import org.springframework.web.context.support.SpringBeanAutowiringSupport;
  12:  
  13: public class AttachmentDownloadServlet extends FileDownloadServlet {
  14:     private static final long serialVersionUID = 1L;
  15:     /**
  16:      * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
  17:      */
  18:     protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  19:         int id = Integer.parseInt(request.getParameter("id"));
  20:
  21:         
  22:     //取出檔案實際存放的路徑,getFilePath請依照個別需實作
  23:         File filePath = new File(getFilePath(id), attachment.getFileName());
  24:     
  25:     //解決微軟存檔名時,會連 "c:/" 一起存    
  26:         File fileName = new File(attachment.getOriginalName());
  27:         sendDownloadFile(response, fullFilePath, fileName.getName());
  28:     }
  29:  
  30:     /**
  31:      * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
  32:      */
  33:     protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  34:         doGet(request, response);
  35:     }


web.xml的設定

   1: <!-- 檔案下載  -->
   2: <servlet>
   3:     <display-name>AttachmentDownloadServlet</display-name>
   4:     <servlet-name>AttachmentDownloadServlet</servlet-name>
   5:     <servlet-class>com.example.servlet.AttachmentDownloadServlet</servlet-class>
   6: <sservlet>
   7:   
   8: <servlet-mapping>
   9:     <servlet-name>AttachmentDownloadServlet</servlet-name>
  10:     <url-pattern>/AttachmentDownload</url-pattern>
  11: <sservlet-mapping>


重點是如何在畫面上,呼叫這隻servlet
這個列子是可以下載多個檔案,的寫法

   1: <c:forEach var="uploaded" items="${ bean.uploadFiles }">
   2:  
   3: <!-- 在jsp中,組出呼叫 servlet 的 url,並在後面加一個id的參數 -->
   4: <c:url value="/AttachmentDownload" var="downloadUrl">
   5:     <c:param name="id" value="${uploaded.id}"></c:param>
   6: </c:url>
   7:  
   8: <!-- 組出來的網址:http://localhost/AttachmentDownload?id=120 -->
   9: <a href="${downloadUrl}"><c:out value="${uploaded.originalName}"/></a>
  10: </forEach>