Servlet – Upload、Download、Async、動態註冊
隨著3.0版本的發布,文件上傳終於成為Servlet規範的一項內置特性,不再依賴於像Commons FileUpload之類組件,因此在服務端進行文件上傳編程變得不費吹灰之力.
客戶端
要上傳文件, 必須利用multipart/form-data設置HTML表單的enctype屬性,且method必須為POST:
<form action="simple_file_upload_servlet.do" method="POST" enctype="multipart/form-data">
<table align="center" border="1" width="50%">
<td>Author:</td>
<td><input type="text" name="author"></td>
</tr>
<tr>
<td>Select file to Upload:</td>
<td><input type="file" name="file"></td>
</tr>
<tr>
<td><input type="submit" value="上傳"></td>
</table>
</form>
服務端
服務端Servlet主要圍繞著@MultipartConfig註解和Part接口:
處理上傳文件的Servlet必須用@MultipartConfig註解標註:
@MultipartConfig屬性 描述
fileSizeThreshold The size threshold after which the file will be written to disk
location The directory location where files will be stored
maxFileSize The maximum size allowed for uploaded files.
在一個由多部件組成的請求中, 每一個表單域(包括非文件域), 都會被封裝成一個Part,HttpServletRequest中提供如下兩個方法獲取封裝好的Part:
HttpServletRequest 描述
Part getPart(String name) Gets the Part with the given name.
Collection<Part> getParts() Gets all the Part components of this request, provided that it is of type multipart/form-data.
Part中提供了如下常用方法來獲取/操作上傳的文件/數據:
Part 描述
InputStream getInputStream() Gets the content of this part as an InputStream
void write(String fileName) A convenience method to write this uploaded item to disk.
String getSubmittedFileName() Gets the file name specified by the client(需要有Tomcat 8.x 及以上版本支持)
long getSize() Returns the size of this fille.
void delete() Deletes the underlying storage for a file item, including deleting any associated temporary disk file.
String getName() Gets the name of this part
String getContentType() Gets the content type of this part.
Collection<String> getHeaderNames() Gets the header names of this Part.
String getHeader(String name) Returns the value of the specified mime header as a String.
文件流解析
通過抓包獲取到客戶端上傳文件的數據格式:
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh
Content-Disposition: form-data; name="author"
feiqing
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh
Content-Disposition: form-data; name="file"; filename="memcached.txt"
Content-Type: text/plain
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh--
可以看到:
A. 如果HTML表單輸入項為文本(<input type="text"/>),將只包含一個請求頭Content-Disposition.
B. 如果HTML表單輸入項為文件(<input type="file"/>), 則包含兩個頭:
Content-Disposition與Content-Type.
在Servlet中處理上傳文件時, 需要:
<code>- 通過查看是否存在Content-Type
標頭, 檢驗一個Part是封裝的普通表單域,還是文件域. - 若有Content-Type
存在, 但文件名為空, 則表示沒有選擇要上傳的文件. - 如果有文件存在, 則可以調用write()
方法來寫入磁盤, 調用同時傳遞一個絕對路徑, 或是相對於@MultipartConfig
註解的location
屬性的相對路徑. </code>
SimpleFileUploadServlet
/**
- @author jifang.
-
@since 2016/5/8 16:27.*/
@MultipartConfig
br/>*/
@MultipartConfig
public class SimpleFileUploadServlet extends HttpServlet {protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter writer = response.getWriter();
Part file = request.getPart("file");
if (!isFileValid(file)) {
writer.print("<h1>請確認上傳文件是否正確!");
} else {
String fileName = file.getSubmittedFileName();
String saveDir = getServletContext().getRealPath("/WEB-INF/files/");
mkdirs(saveDir);
file.write(saveDir + fileName);writer.print("<h3>Uploaded file name: " + fileName); writer.print("<h3>Size: " + file.getSize()); writer.print("<h3>Author: " + request.getParameter("author")); }
}
private void mkdirs(String saveDir) {
File dir = new File(saveDir);
if (!dir.exists()) {
dir.mkdirs();
}
}private boolean isFileValid(Part file) {
// 上傳的並非文件
if (file.getContentType() == null) {
return false;
}
// 沒有選擇任何文件
else if (Strings.isNullOrEmpty(file.getSubmittedFileName())) {
return false;
}
return true;
}
}
優化
善用WEB-INF
存放在/WEB-INF/目錄下的資源無法在瀏覽器地址欄直接訪問, 利用這一特點可將某些受保護資源存放在WEB-INF目錄下, 禁止用戶直接訪問(如用戶上傳的可執行文件,如JSP等),以防被惡意執行, 造成服務器信息泄露等危險.
getServletContext().getRealPath("/WEB-INF/")
文件名亂碼
當文件名包含中文時,可能會出現亂碼,其解決方案與POST相同:
1
request.setCharacterEncoding("UTF-8");
避免文件同名
如果上傳同名文件,會造成文件覆蓋.因此可以為每份文件生成一個唯一ID,然後連接原始文件名:
private String generateUUID() {
return UUID.randomUUID().toString().replace("-", "_");
}
目錄打散
如果一個目錄下存放的文件過多, 會導致文件檢索速度下降,因此需要將文件打散存放到不同目錄中, 在此我們采用Hash打散法(根據文件名生成Hash值, 取Hash值的前兩個字符作為二級目錄名), 將文件分布到一個二級目錄中:
1
2
3
4
private String generateTwoLevelDir(String destFileName) {
String hash = Integer.toHexString(destFileName.hashCode());
return String.format("%s/%s", hash.charAt(0), hash.charAt(1));
}
采用Hash打散的好處是:在根目錄下最多生成16個目錄,而每個子目錄下最多再生成16個子子目錄,即一共256個目錄,且分布較為均勻.
示例-簡易存儲圖片服務器
需求: 提供上傳圖片功能, 為其生成外鏈, 並提供下載功能(見下)
客戶端
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>IFS</title>
</head>
<body>
<form action="ifs_upload.action" method="POST" enctype="multipart/form-data">
<table align="center" border="1" width="50%">
<tr>
<td>Select A Image to Upload:</td>
<td><input type="file" name="image"></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" value="上傳"></td>