文件上傳與下載
1. 文件上傳
為了能上傳文件,必須將表單的 method
設置為 POST
,并將 enctype
設置為 multipart/form-data
。
有兩種實現文件上傳的方式:
-
底層使用 Apache Commons FileUpload 包
-
底層使用 Servlet 3.1 內置的文件上傳功能
無論是哪種方式,其使用方式都是一樣的,將 file 類型的請求參數綁定到請求處理方法的特定類型的參數上:
-
CommonsMultipartFile(commons-fileupload)
-
MultipartFile(Servlet 3.1)
Web 3.0 的文件上傳
普通的表單(form)元素無法直接上傳文件,必須通過「特殊處理」。
對上傳文件功能而言,有些特殊的地方:
- form 表單內,要添加控件
<input type="file" name="myfile">
- form 表單的提交方式必須是 post 方式
- form 表單的內容格式要定義成 multipart/form-data 格式
<form action="..." method="post" enctype="multipart/form-data">...
</form>
enctype="multipart/form-data"
表示表單元素中的 input 數據以二進制的方式發送到服務端。此時如果是普通的 input 數據,無法像之前一樣從 request 中直接獲得。
對于上傳文件的的大小問題,慣例是:
- 足夠小的文件,先接收到內存中,最后寫入磁盤。
- 稍大的文件,寫入磁盤臨時文件,最后寫入最終目的地。
- 大型文件,禁止上傳。
在 Web 3.0 之前 使用 commons-fileupload 庫是最常見的上傳辦法。當 Servlet 的設計者意識到文件上傳的重要性后,在 Web 3.0 中它就成了一項內置的特性。
Web 3.0 中的文件上傳主要圍繞著 MultipartConfig 注解和 Part 接口。
@MultipartConfig 注解
屬性 | 說明 |
---|---|
fileSizeThreshold 可選屬性 | 超過該值大小的文件,在上傳過程中,將被寫入磁盤臨時文件,而不是保存在內存中。 |
maxFileSize 可選屬性 | 每個上傳文件的大小上限。 |
maxRequestSize 可選屬性 | 一次請求(可能包含多個上傳)的大小上限。 |
@WebServlet(urlPatterns="/hello.do")
@MultipartConfig(maxFileSize = 5*1024*1024)
public class HelloServlet extends HttpServlet {...
}
Part 接口
在一個表單(Form)中,無論是否有文件上傳控件,Servlet 3.0 都會將這些表單控件對應成代碼中的一個 Part 對象。
通過 request 對象的 .getParts()
方法可以獲得所有的這些 Part 對象。
Collection<Part> parts = request.getParts();
在一個或多個部分組成的請求中,每一個表單域(包括非文本域),都將被轉換成一個 Part 。
普通文本域和文件上傳域的區別在于,其 Part 對象的 .getContentType()
方法返回值的不同。對于普通文本域的 Part 對象而言,該方法返回 null 。
for (Part part : parts) {if (part.getContentType() == null) {System.out.println("普通文本域");}else {System.out.println("文件上傳域");}
}
補充,如果是要獲取普通文本域的值,其實直接使用正常 request.getParameter()
就行。
每一個 Part 分為「頭」和「體」兩部分。普通文本域只有頭部,而文件上傳域則有頭有體。
普通文本域的頭部形式為:
content-disposition:form-data; name="域名"
上傳文本域的頭部形式為:
content-type:內容類型
content-disposition:form-data; name="域名"; filename="文件名"
對我們而言,需要的是文本上傳域中的 content-disposition 中的 filename 部分。
String header = part.getHeader("content-disposition");
// 內容為 form-data; name="域名"; filename="文件名"
通常會使用工具類,將 content-disposition 中的 filename 中的值截取出來。
private String getFileName(String header) {String[] arr = header.split("; ");String item = null;for (String cur : arr) {// System.out.println("debug: " + cur);if (cur.startsWith("filename=")) {item = cur;break;}}int start = item.indexOf('"')+1;int end = item.lastIndexOf('"');String filename = item.substring(start, end);// System.out.println(filename);return filename;
}
Part 對象直接提供了方法將上傳文件的內容寫入盤:
String savePath = request.getServletContext().getRealPath("/WEB-INF/uploadFile/");
String filePathName = savePath + File.separator + fileName; // 目標文件路徑名
part.write(filePathName); // 把文件寫到指定路徑
Part 的其它常用方法
方法 | 說明 |
---|---|
getContentType() | 獲得 Part 的內容類型。如果 Part 是普通文本,那么返回 null 。 該方法可以用于區別是普通文本域,還是文件上傳域。 |
getHeader() | 該方法用于獲取指定的標頭的值。 對于上傳文本域的 Part,該參數有 content-type 和 content-disposition 對于普通文本域,則只有 content-disposition 一種。 |
getName() | 無論是普通文本域 Part ,還是文件上傳域 Part ,都是獲得域名值。 |
write() | 將上傳文件寫入磁盤中。 |
delete() | 手動刪除臨時文件 |
getInputStream() | 以 InputStream 形式返回上傳文件的內容。 |
利用 commons-fileupload 文件上傳
利用 commons-fileupload 文件上傳需要利用引入 commons-fileupload 包(它依賴于 commons-io 包)
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version>
</dependency>
作為 Servlet 內置上傳功能之前的『準標準』,Servlet 在引入內置上傳功能時借鑒了 commons-fileupload 的實現方式。因此,在了解 Servlet 內置上傳功能之后,再回頭看 commons-fileupload 文件上傳時,你會發現它們的基本邏輯/大道理時一樣的,只不過 commons-fileupload 的實現會羅嗦一些
在 Servlet 內置的上傳功能中,從 request 中獲得的是一個 Part[]
,其中的每一個 Part 對象對應著表單中的一個表單域(Form Field)。而 commons-fileupload 中我們從 request 中獲得的是一個 List<FileItem>
,commons-fileupload 中使用 FileItem 來對應每一個表單域,起到和 Part 一樣的作用。
commons-fileupload 的羅嗦體現在以下幾個方面:
- commons-fileupload 不能對 Servlet 使用注解,因此相關的上傳配置需要通過編碼實現。
- commons-fileupload 不能使用
request.getParameter()
為了能夠從 request 中獲得 List<FileItem>
,你需要兩個對象:
// 創建上傳所需要的兩個對象
DiskFileItemFactory factory = new DiskFileItemFactory(); // 磁盤文件對象
ServletFileUpload sfu = new ServletFileUpload(factory); // 文件上傳對象
如果不做出設置,那么相關設置則采用默認值。
// 設置上傳過程中所收到的數據是『存內存』還是『存磁盤』的閾值
factory.setSizeThreshold(100 * 1024);
// 設置磁盤臨時文件的保存目錄
factory.setRepository(new File("D:/upload"));// 設置解析文件上傳中的文件名的編碼格式,解決上傳文件名中文亂碼問題
sfu.setHeaderEncoding("utf-8");
// 限制單個文件的大小
sfu.setFileSizeMax(10*1024);
// 限制上傳的總文件大小
sfu.setSizeMax(100*1024);
在創建文件上傳對象(并作出相應設置)之后,我們可以通過它從 request 中獲取我們所需要的 List<FileItem>
。
List<FileItem> list = sfu.parseRequest(request);
FileItem 自帶了方法,可以判斷當前的 FileItem 對應的是頁面上的普通文本域,還是文件上傳域:
for (FileItem item : list) {if (item.isFormField()) {System.out.println("普通文本域");}else { System.out.println("文件上傳域");}
}
由于 commons-fileupload 中無法使用 request.getParameter()
,因此,為了獲得普通文本域中的數據,需要使用 FileItem 自己的方法:
for (FileItem item : list) {if (item.isFormField()) {String fieldName = item.getFieldName(); // 例如:username / passwordString fieldValue = item.getString("UTF-8"); // 例如,tom / 123456System.out.println(fieldName + ": " + fieldValue);}else { System.out.println("文件上傳域");}
}
由于 commons-fileupload 引用了 commons-io,所以,將上傳的文件內容寫入磁盤倒是十分簡單:
for (FileItem item : list) {if (item.isFormField()) {...}else { System.out.println("文件上傳域");// 創建輸出文件String name = item.getName();// 獲取上傳文件的名字String outPath = "D:/upload/" + name;FileOutputStream output = new FileOutputStream(new File(outPath));// 獲得上傳文件字節流InputStream input = item.getInputStream(); // 使用 IOUtils 工具將輸入流中的數據寫入到輸出流。IOUtils.copy(input, output);System.out.println("上傳成功!保存的路徑為:" + outPath);input.close(); // 關閉輸入流output.close(); // 關閉輸出流item.delete(); // 刪除處理文件上傳時生成的臨時文件}
}
2. 文件下載
內容類型 | 文件擴展名 | 描述 |
---|---|---|
text/plain | txt | 文本文件(包括但不僅包括txt) |
application/msword | doc | Microsoft Word |
application/pdf | Adobe Acrobat | |
application/zip | zip | winzip |
audio/mpeg | mp3 | mp3 音頻文件 |
image/gif | gif | COMPUSERVE GIF 圖像 |
image/jpeg | jpeg jpg | JPEG 圖像 |
image/png | png | PNG 圖像 |
詳細 MIME 參見 網址 。
相對于上傳而言,下載文件較為簡單,只需要完成兩步:
- 設置響應的內容類型。
- 添加一個
content-disposition
到響應標頭(addHeader()
方法),其值為:attachment; filename=文件名
- 通過 resp 對象獲得輸出流,將文件內容發送至客戶端。
resp.setContentType("text/plain"); // step 1
resp.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode("D:/note.txt", "UTF-8")); // step 2InputStream is = new FileInputStream(new File("D:/note.txt"));
OutputStream out = resp.getOutputStream();byte[] buffer = new byte[1024];
int n = 0;
while ((n = is.read(buffer)) > 0) {out.write(buffer, 0, n); // step 3
}is.close();
out.close();
System.out.println("下載成功");