1. 程式人生 > >JavaWeb之檔案上傳與下載詳解

JavaWeb之檔案上傳與下載詳解

檔案上傳

檔案上傳概述

  • 實現web開發中的檔案上傳功能,需完成如下二步操作:

    (1)在web頁面中新增上傳輸入項;
    (2)在servlet中讀取上傳檔案的資料,並儲存到本地硬碟中。

  • 如何在web頁面中新增上傳輸入項?<input type=”file”>標籤用於在web頁面中新增檔案上傳輸入項,設定檔案上傳輸入項時須注意:

    (1)必須要設定input輸入項的name屬性,否則瀏覽器將不會發送上傳檔案的資料;
    (2)必須把form的enctype屬值設為multipart/form-data.設定該值後,瀏覽器在上傳檔案時,將把檔案資料附帶在http請求訊息體中,並使用MIME協議對上傳的檔案進行描述,以方便接收方對上傳資料進行解析和處理。

  • 如何在Servlet中讀取檔案上傳資料,並儲存到本地硬碟中?

    (1)Request物件提供了一個getInputStream方法,通過這個方法可以讀取到客戶端提交過來的資料。但由於使用者可能會同時上傳多個檔案,在servlet端程式設計直接讀取上傳資料,並分別解析出相應的檔案資料是一項非常麻煩的工作;
    (2)為方便使用者處理檔案上傳資料,Apache 開源組織提供了一個用來處理表單檔案上傳的一個開源元件( Commons-fileupload ),該元件效能優異,並且其API使用極其簡單,可以讓開發人員輕鬆實現web檔案上傳功能,因此在web開發中實現檔案上傳功能,通常使用Commons-fileupload元件實現。

  • 使用Commons-fileupload元件實現檔案上傳,需要匯入該元件相應的支撐jar包:Commons-fileupload和commons-io。commons-io 不屬於檔案上傳元件的開發jar檔案,但Commons-fileupload 元件從1.1 版本開始,它工作時需要commons-io包的支援。

檔案上傳原理

      對請求正文是multipart/form-data型別的資料進行解析。

fileupload工作流程

fileupload核心API

DiskFileItemFactory工廠類

      DiskFileItemFactory 是建立 FileItem 物件的工廠,這個工廠類常用方法:

  • public DiskFileItemFactory(int sizeThreshold, java.io.File repository):構造方法;
  • public void setSizeThreshold(int sizeThreshold) :設定記憶體緩衝區的大小,預設值為10K。當上傳檔案大於緩衝區大小時, fileupload元件將使用臨時檔案快取上傳檔案。
  • public void setRepository(java.io.File repository) :指定臨時檔案目錄,預設值為當前使用者的系統臨時檔案目錄,可通過System.getProperty(“java.io.tmpdir”)列印檢視;

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) :設定編碼格式;

FileItem表單欄位域物件

      FileItem類負責處理表單提交的欄位,常用方法有:

1、處理普通欄位方法

  • boolean isFormField() //判斷是否為普通欄位;
  • String getFieldName() //返回表單欄位(普通欄位)名稱;
  • String getString() //返回普通欄位的值;
  • String getString(String encoding) //返回普通欄位的值,並設定請求引數編碼,常用於get提交

2、處理上傳欄位方法

  • String getContentType() //返回表單請求型別,即request請求頭中的Content-type的內容,可用來限制檔案上傳的型別;
  • String getName() //返回檔名(全路徑名:例如C:\a.txt);
  • InputStream getInputStream() //返回表單輸入流物件;
  • void write(File file) //將上傳檔案寫入到磁碟;
  • void delete() //刪除該上傳欄位儲存在磁碟上的臨時檔案;

檔案上傳案例

實現步驟

  1. 建立DiskFileItemFactory物件,設定緩衝區大小和臨時檔案目錄;
  2. 使用DiskFileItemFactory 物件建立ServletFileUpload物件,並設定上傳檔案的大小限制;
  3. 呼叫ServletFileUpload.parseRequest方法解析request物件,得到一個儲存了所有上傳內容的List物件;
  4. 對list進行迭代,每迭代一個FileItem物件,呼叫其isFormField方法判斷是否是上傳檔案;如果為普通表單欄位,則呼叫getFieldName、getString方法得到欄位名和欄位值;如果為上傳檔案,則呼叫getInputStream方法得到資料輸入流,從而讀取上傳資料。

上傳檔案的處理細節

1、中文檔案亂碼問題

      檔名中文亂碼問題,可呼叫ServletUpLoader的setHeaderEncoding方法,或者設定request的setCharacterEncoding屬性

2、臨時檔案的刪除問題

  • 由於檔案大小超出DiskFileItemFactory.setSizeThreshold方法設定的記憶體緩衝區的大小時,Commons-fileupload元件將使用臨時檔案儲存上傳資料,因此在程式結束時,務必呼叫FileItem.delete方法刪除臨時檔案;
  • Delete方法的呼叫必須位於流關閉之後,否則會出現檔案佔用,而導致刪除失敗的情況;

3、檔案存放位置

  • 為保證伺服器安全,上傳檔案應儲存在應用程式的WEB-INF目錄下,或者不受WEB伺服器管理的目錄;
  • 為防止多使用者上傳相同檔名的檔案,而導致檔案覆蓋的情況發生,檔案上傳程式應保證上傳檔案具有唯一檔名;
  • 為防止單個目錄下檔案過多,影響檔案讀寫速度,處理上傳檔案的程式應根據可能的檔案上傳總量,選擇合適的目錄結構生成演算法,將上傳檔案分散儲存;

4、顯示上傳進度

(1)ProgressListener顯示上傳進度

ProgressListener progressListener = new ProgressListener() {
    public void update(long pBytesRead, long pContentLength, int pItems) {
        System.out.println("到現在為止,  " + pBytesRead + " 位元組已上傳,總大小為 "
              + pContentLength);
    }
};
upload.setProgressListener(progressListener);

(2)以KB為單位顯示上傳進度

long temp = -1;   //temp注意設定為類變數
long ctemp = pBytesRead /1024; 
if (mBytes == ctemp)  
    return; 
temp = mBytes; 

編碼實現

      使用工具類:GUIDUtils.java、DirectoryUtils.java

package com.study.java.utils;

import java.math.BigInteger;
import java.util.Random;

/**
* @Name: GUIDUtils
* @Description: 建立GUID唯一字串工具類,參考struts2原始碼
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class GUIDUtils {

    public static String generateGUID() {
        return new BigInteger(165, new Random()).toString().toUpperCase() ;
    }
}
package com.study.java.utils;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* @Name: DirectoryUtils
* @Description: 目錄建立工具類
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class DirectoryUtils {

    /**
    * @Name: getChildDirectoryByDate
    * @Description: 根據當前日期建立子目錄
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param storeDirectory 檔案儲存主目錄
    * @Return: String
     */
    public static String getChildDirectoryByDate(String storeDirectory) {
        Date now = new Date() ;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd") ;
        String childDirectory = sdf.format(now) ;
        File file = new File(storeDirectory, childDirectory) ;
        if(!file.exists()) {
            file.mkdirs() ;
        }
        return childDirectory ;
    }

    /**
    * @Name: getChildDirectoryByHashCode
    * @Description: 根據檔名稱字串的hash碼建立二級子目錄
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param storeDirectory 檔案儲存主目錄
    * @Parameters: @param filename 檔名稱
    * @Return: String
     */
    public static String getChildDirectoryByHashCode(String storeDirectory, String filename) {
        int hashCode = filename.hashCode() ;
        int dir1 = hashCode & 0xf ;
        int dir2 = (hashCode & 0xf0) >> 4 ;
        String childDirectory = dir1 + "/" + dir2 ;
        File file = new File(storeDirectory, childDirectory) ;
        if(!file.exists()) {
            file.mkdirs() ;
        }
        return childDirectory ;
    }
}

      實現檔案上傳程式碼:UploadServlet.java

package com.study.java.servlet;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import com.study.java.utils.DirectoryUtils;
import com.study.java.utils.GUIDUtils;

/**
* @Name: UploadServlet
* @Description: 檔案上傳
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class UploadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8") ;
        PrintWriter out = response.getWriter() ;
        //設定請求引數編碼,適用於post提交,用於處理上傳檔案中文編碼問題
        request.setCharacterEncoding("UTF-8") ;
        //1、建立FileItem工廠物件
        DiskFileItemFactory factory = new DiskFileItemFactory() ;
        //設定緩衝區大小1M,預設10kb
        factory.setSizeThreshold(1*1024*1024) ;
        //設定臨時檔案存放目錄,預設為當前使用者系統臨時檔案存放目錄
        String template = getServletContext().getRealPath("/WEB-INF/template") ;
        File templatePath = new File(template) ;
        if(!templatePath.exists()) {
            templatePath.mkdirs() ;
        }
        factory.setRepository(templatePath) ;
        //2、建立上傳檔案核心解析器物件
        ServletFileUpload sfu = new ServletFileUpload(factory) ;
        //判斷表單提交型別是否為multipart/form-data
        boolean isMultipartContent = sfu.isMultipartContent(request) ;
        if(!isMultipartContent) {
            out.write("表單提交型別不正確!!!") ;
        }
        //限制單檔案上傳大小
        sfu.setFileSizeMax(3*1024*1024) ;
        //限制一次上傳總檔案大小
        sfu.setSizeMax(10*1024*1024) ;
        //3、解析表單提交引數
        try {
            List<FileItem> items = sfu.parseRequest(request) ;
            //獲取表單提交引數資訊物件
            for (FileItem item : items) {
                if(item.isFormField()) {
                    //普通欄位
                    processFormField(item) ;
                } else {
                    //上傳欄位
                    processUploadField(item) ;
                }
            }
            out.write("上傳成功!!!") ;
            response.setHeader("Refresh", "2;URL=" + request.getContextPath()) ;
        } catch (FileUploadBase.FileSizeLimitExceededException e) {
            out.write("單檔案上傳大小超出限制3M.") ;
        } catch (FileUploadBase.SizeLimitExceededException e) {
            out.write("總檔案上傳大小超出限制10M.") ;
        } catch (FileUploadException e) {
            out.write("表單提交引數解析失敗!!!") ;
        }
    }

    /**
    * @Name: processUploadField
    * @Description: 上傳表單提交的檔案
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param item
    * @Return: void
     */
    private void processUploadField(FileItem item) {
        //獲取檔案上傳主目錄
        String storeDirectory = getServletContext().getRealPath("/WEB-INF/upload") ;
        File file = new File(storeDirectory) ;
        if(!file.exists()) {
            file.mkdirs() ;
        }
        //獲取上傳檔名
        String filename = item.getName() ;
        if(filename != null && !"".equals(filename.toString().trim())) {
            filename = GUIDUtils.generateGUID() + "_" + filename.substring(filename.lastIndexOf("//") + 1) ;
        }
        //獲取檔案上傳子目錄
        String childDirectory = DirectoryUtils.getChildDirectoryByHashCode(storeDirectory, filename) ;
        File f = new File(storeDirectory + "/" + childDirectory, filename) ;
        //上傳
        try {
            item.write(f) ;
        } catch (Exception e) {
            throw new RuntimeException("檔案上傳失敗!!!") ;
        }
    }

    /**
    * @Name: processFormField
    * @Description: 處理表單提交普通欄位,列印到控制檯
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param item
    * @Return: void
     */
    private void processFormField(FileItem item) {
        try {
            String fieldName = item.getFieldName() ;
            String fieldValue = item.getString("UTF-8") ;
            System.out.println(fieldName + "->" + fieldValue);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doGet(request, response);
    }
}

檔案下載

Web應用中實現檔案下載的兩種方式

  1. 超連結直接指向下載資源;

  2. 程式實現下載:程式實現下載需設定兩個響應頭,

    (1)設定Content-Type 的值為:application/x-msdownload。Web 伺服器需要告訴瀏覽器其所輸出的內容的型別不是普通的文字檔案或 HTML 檔案,而是一個要儲存到本地的下載檔案;

    (2)Web 伺服器希望瀏覽器不直接處理相應的實體內容,而是由使用者選擇將相應的實體內容儲存到一個檔案中,這需要設定 Content-Disposition 報頭。該報頭指定了接收程式處理資料內容的方式,在 HTTP 應用中只有 attachment 是標準方式,attachment 表示要求使用者干預。在 attachment 後面還可以指定 filename 引數,該引數是伺服器建議瀏覽器將實體內容儲存到檔案中的檔名稱。在設定 Content-Dispostion 之前一定要指定 Content-Type。

    (3)因為要下載的檔案可以是各種型別的檔案,所以要將檔案傳送給客戶端,其相應內容應該被當做二進位制來處理,所以應該呼叫response.getOutputStream()方法返回 ServeltOutputStream 物件來向客戶端寫入檔案內容。

response.setContentType("application/x-msdownload") ;
String str = "attachment;filename=" + java.net.URLEncoder.encode(fileName, "UTF-8") ;
response.setHeader("Content-Disposition", str) ;
InputStream is = new FileInputStream(file) ;
OutputStream os = response.getOutputStream() ;
int len = -1 ;
byte[] buf = new byte[1024] ;
while((len = is.read(buf)) != -1) {
    os.write(buf, 0, len) ;
    os.flush() ;
}
is.close() ;
os.close() ;

下載案例

      遍歷上傳目錄下的所有檔案顯示給使用者,並允許使用者完成下載。

(1)ShowAllUploadFiles.java

package com.study.java.servlet;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @Name: ShowAllUploadFiles
* @Description: 顯示所有已經上傳的檔案
* @Author: XXX
* @CreateDate: XXX 
* @Version: V1.0
 */
public class ShowAllUploadFiles extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Map<String, String> map = new HashMap<String, String>() ;
        //獲取檔案上傳主目錄
        String storeDirectory = getServletContext().getRealPath("/WEB-INF/upload") ;
        File file = new File(storeDirectory) ;
        treeWalk(file, map) ;
        request.setAttribute("map", map) ;
        request.getRequestDispatcher("/listFiles.jsp").forward(request, response) ;
    }

    /**
    * @Name: treeWalk
    * @Description: 將檔案上傳主目錄中的所有檔名稱放入到Map集合中
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param file
    * @Parameters: @param map
    * @Return: void
     */
    private void treeWalk(File file, Map<String, String> map) {
        if(file.isDirectory()) {
            File[] list = file.listFiles() ;
            for (File f : list) {
                treeWalk(f, map) ;
            }
        } else {
            String guidFileName = file.getName() ;
            String oldFileName = guidFileName.substring(guidFileName.indexOf("_") + 1) ;
            map.put(guidFileName, oldFileName) ;
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doGet(request, response);
    }
}

(2)JSP檢視頁面

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title></title>
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
      </head>
  <body>
    <c:forEach items="${map}" var="me">
        <c:url value="/servlet/DownloadServlet" var="url">
            <c:param name="guidFileName" value="${me.key}"/>
        </c:url><hr/>
        ${me.value}
        &nbsp;&nbsp;
        <a href="${url}">下載</a><hr/>
    </c:forEach>
  </body>
</html>

(3)DownloadServlet.java

package com.study.java.servlet;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URLEncoder;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.study.java.utils.DirectoryUtils;

/**
* @Name: DownloadServlet
* @Description: 檔案下載
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class DownloadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //獲取請求連結中的引數,並進行編碼,解決檔名中帶中文的情況
        String guidFileName = request.getParameter("guidFileName") ;
        guidFileName = new String(guidFileName.getBytes("ISO-8859-1"), "utf-8") ;
        //獲取檔案儲存目錄
        String storeDirectory = getServletContext().getRealPath("/WEB-INF/upload") ;
        String childDirectory = DirectoryUtils.getChildDirectoryByHashCode(storeDirectory, guidFileName) ;
        File file = new File(storeDirectory + "/" + childDirectory, guidFileName) ;
        if(file.exists()) {
            //通知客戶端下載
            String oldFileName = guidFileName.substring(guidFileName.indexOf("_") + 1) ;
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(oldFileName, "UTF-8")) ;
            //下載
            InputStream is = new FileInputStream(file) ;
            OutputStream os = response.getOutputStream() ;
            int len = -1 ;
            byte[] buf = new byte[1024] ;
            while((len = is.read(buf)) != -1) {
                os.write(buf, 0, len) ;
                os.flush() ;
            }
            is.close() ;
            os.close() ;
        } else {
            throw new RuntimeException("下載檔案不存在!!!") ;
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doGet(request, response);
    }
}