Java Web基礎知識之檔案上傳:檔案上傳一窺究竟
阿新 • • 發佈:2018-12-31
其實檔案上傳的文章已經寫得很多了,但是好多文章都是都是說明了怎麼實現,沒有說這個過程到底發生了什麼(會不會引來仇恨。。),其實實現檔案上傳並不複雜,也沒有多少程式碼,但是要是清楚的明白其中的原理還是費點功夫的,這裡就還原檔案上傳的整個過程。
其實關於檔案上傳在最早之前是使用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來上傳多個檔案了;
這裡邊最重要的就是Content-type,這個型別和表單的enctype型別相同,最重要的就是增加了boundary屬性,該屬性的值就是用來分割表單中各個部分的。 下面是post表單時發出的request payload,如下:
從圖中可以看出整個被上傳的表單資料是被分隔符包裹起來,並且通過使用"分隔符--"的方式來標明資料結束,這個分隔符在開頭和結尾必須有又來說明資料的開始和結束,只有在表單中有多個元素或者上傳多個檔案時才會在中間出現,每一個被分隔符分隔的部分裡面都包含Content-disposition首部,裡面包含表單元素中的一些屬性,有name,filename;但是content-type首部是可選的,而且對於表單中非檔案的部分是沒有content-type的,只有檔案域才會有content-type這個首部
二、 服務端程式設計
瞭解客戶端是為了我們在服務端解析客戶端發過來的請求,那麼如何判斷髮過來的請求中是否包含檔案呢?基於以下幾點可以進行判斷:- 在一個由multipart/form-data組成的請求中,每一個部分包括非檔案部分都會轉換成一個Part物件,在伺服器端我們主要是針對該Part物件進行處理;
- 通過檢視Part中是否存在content-type首部來判斷一個Part是屬於普通的非檔案部分,還是屬於檔案部分;
- 如果存在content-type,則說明檔案部分存在,之後檢視上傳的檔名稱是否為空,檔名為空說明有客戶端沒有選擇要上傳的檔案;
- 如果檔案存在,就使用Part的write方法來將他寫入伺服器端的檔案系統;
@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;
三、 其他問題
上面兩個部分主要就是說明了客戶端和服務端分別怎麼做,但是根據具體的業務邏輯還有很多別的需求需要考慮,如下:- 對檔案的字尾名進行驗證和約束;
- 對檔案的大小進行約束;
- 檔案的儲存,是放在本地檔案系統上還是資料庫中;
- 檔案的編碼問題,尤其是中文的編碼問題;
- 避免相同檔名的檔案的重複上傳導致覆蓋問題;