1. 程式人生 > >Java Web基礎知識之檔案上傳:檔案上傳一窺究竟

Java Web基礎知識之檔案上傳:檔案上傳一窺究竟

其實檔案上傳的文章已經寫得很多了,但是好多文章都是都是說明了怎麼實現,沒有說這個過程到底發生了什麼(會不會引來仇恨。。),其實實現檔案上傳並不複雜,也沒有多少程式碼,但是要是清楚的明白其中的原理還是費點功夫的,這裡就還原檔案上傳的整個過程。

其實關於檔案上傳在最早之前是使用Apache的Commons FileUpload元件,但是自從servlet提出了自己的解決辦法之後,就不再使用這個元件了,有了正規軍誰還使用民兵啊,不對,也不一定,之前Apache的HttpClient就比JDK自己的HttpUrlConnection流行,不說廢話了,直接進入!

一、 客戶端程式設計

下面是我們的頁面FileUpload.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>File upload</title>
</head>
<body>
	<form action="/JavaServlet/fileUploadServlet" method="post" enctype="multipart/form-data">
		select a file :<input type="file" name="file" multiple>
		<input type="text" value="upload file" name="identifier" />
		<input type="submit" value="upload" />
	</form>
</body>
</html>
關於這個頁面有幾點值得注意:
  • 首先是action是URI,注意它和URL的區別,不要省掉contextPath,這樣會找不到該資源的;
  • enctype的值一定是multipart/form-data,這個屬性是指在傳送放到伺服器之前如何對錶單資料進行編碼,這種方式是將表單資料組裝成一條訊息,並用分隔符將表單的每個部分分隔開;預設值是application/x-www-form-urlencoded,也意味著所有的值都會進行編碼,這種方式是用鍵值對來進行編碼;
  • 如果想要上傳多個檔案,可以使用multiple屬性,注意這個屬性是在HTML5中提出的,這樣就不用我們使用多個input來上傳多個檔案了;
下面我上傳三個檔案,可以看到Http請求和響應如下,我使用的是chrome瀏覽器:

這裡邊最重要的就是Content-type,這個型別和表單的enctype型別相同,最重要的就是增加了boundary屬性,該屬性的值就是用來分割表單中各個部分的。 下面是post表單時發出的request payload,如下:
從圖中可以看出整個被上傳的表單資料是被分隔符包裹起來,並且通過使用"分隔符--"的方式來標明資料結束,這個分隔符在開頭和結尾必須有又來說明資料的開始和結束,只有在表單中有多個元素或者上傳多個檔案時才會在中間出現,每一個被分隔符分隔的部分裡面都包含Content-disposition首部,裡面包含表單元素中的一些屬性,有name,filename;但是content-type首部是可選的,而且對於表單中非檔案的部分是沒有content-type的,只有檔案域才會有content-type這個首部
。 值得注意的是,當沒有檔案上傳的時候仍然會有檔案部分存在,只不過filename屬性為空,如下:

二、 服務端程式設計

瞭解客戶端是為了我們在服務端解析客戶端發過來的請求,那麼如何判斷髮過來的請求中是否包含檔案呢?基於以下幾點可以進行判斷:
  • 在一個由multipart/form-data組成的請求中,每一個部分包括非檔案部分都會轉換成一個Part物件,在伺服器端我們主要是針對該Part物件進行處理;
  • 通過檢視Part中是否存在content-type首部來判斷一個Part是屬於普通的非檔案部分,還是屬於檔案部分;
  • 如果存在content-type,則說明檔案部分存在,之後檢視上傳的檔名稱是否為空,檔名為空說明有客戶端沒有選擇要上傳的檔案;
  • 如果檔案存在,就使用Part的write方法來將他寫入伺服器端的檔案系統;
在伺服器上處理檔案上傳的servlet如下:
@WebServlet(name="fileUploadServlet", urlPatterns={"/fileUploadServlet"})
//@MultipartConfig(location="/")
@MultipartConfig
public class FileUploadServlet extends HttpServlet {

	private static final long serialVersionUID = 1920423365061691218L;
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Collection<Part> parts = req.getParts();
		
		for(Part part: parts){
			if(part.getContentType() != null){
				String filename = getFileName(part);
				if(filename != null && !filename.isEmpty()){
					part.write(filename);
				}
			} 
		}
	}
	
	
	String getFileName(Part part){
		Objects.requireNonNull(part, "part can not be null");
		
		String disposition = part.getHeader("content-disposition");
		String[] disParts = disposition.split(";");
		
		String filenamePart = disParts[disParts.length - 1];
		String filename = filenamePart.substring(filenamePart.indexOf("=")+1).trim().replace("\"", "");
		
		return filename;
	}
}
關於上述的處理其實主要圍繞@MultipartConfig註解和Part介面來進行,關於這兩個的使用其實很簡單,可以檢視一下JavaDoc即可,但是有兩個我要著重說一下,因為我自己就掉坑裡了:
  • 一個就是@MultipartConfig中的location屬性,這個絕對是一個坑,當這個值是一個絕對路徑時,呼叫Part的write()方法將該檔案寫到對應的路徑是沒有問題的,但是當是相對路徑的時候,比如如我上邊寫的"/",這個相對路徑是相對於tomcat路徑下的C:\Program Files\tomcat7\work\Catalina\localhost\JavaServlet路徑的,這是一個檔案上傳臨時儲存的位置,這個路徑值主要是為了在檔案超過預設大小時寫入硬碟,為@MultiSizeThreshold準備,所以最好還是不要使用這個屬性為好;
  • 還有一個就是Part中的getName()方法並不是用來獲取檔名的,而是用來獲取表單元素中的name屬性的;檔名需要我們自己來解析出來;
  • 在使用Part的write()方法時,如果提供的是相對路徑,那麼相對路徑的根路徑都是C:\Program Files\tomcat7\work\Catalina\localhost\JavaServlet;

三、 其他問題

上面兩個部分主要就是說明了客戶端和服務端分別怎麼做,但是根據具體的業務邏輯還有很多別的需求需要考慮,如下:
  • 對檔案的字尾名進行驗證和約束;
  • 對檔案的大小進行約束;
  • 檔案的儲存,是放在本地檔案系統上還是資料庫中;
  • 檔案的編碼問題,尤其是中文的編碼問題;
  • 避免相同檔名的檔案的重複上傳導致覆蓋問題;
這些問題實現起來其實都比較簡單,下面有一篇文章可以進行參考,主要是怕考慮不到這些問題,或者為了省事偷工減料。