java web 檔案上傳的總結
前言
1. 關於enctype=”multipart/form-data”:
對於表單enctype屬性,w3school解釋如下:規定在傳送到伺服器之前應該如何對錶單資料進行編碼,預設的,表單資料會編碼為 “application/x-www-form-urlencoded” 就是說,在傳送到伺服器之前,所有字元都會進行編碼(空格轉換為 “+” 加號,特殊符號轉換為 ASCII HEX 值)。
另外,application/x-www-form-urlencoded(預設)的POST資料是放在請求體的Form Data中,其他都是放在Request Payload 中。至於在上傳控制元件的表單一定用multipart/form-data,因為不知道使用者上傳的是什麼,如果對其字元編碼後伺服器再解碼是很費時且容易出錯的。
2. 關於表單裡普通文字域request.getParameter()獲取不到值?
原因:由於Tomcat對於Content-Type multipart/form-data(檔案上傳)和application/x-www-form-urlencoded(普通POST請求)做了“特殊處理”。下面來看看相關的處理程式碼。
Tomcat的HttpServletRequest類的實現類為org.apache.catalina.connector.Request(實際上是org.apache.coyote.Request),而它對處理請求引數的方法為protected void parseParameters(),這個方法中對Content-Type multipart/form-data(檔案上傳)和application/x-www-form-urlencoded(POST請求)的處理程式碼如下:
protectedvoid parseParameters() {
//省略部分程式碼......
parameters.handleQueryParameters();// 這裡是處理url中的引數
//省略部分程式碼......
if ("multipart/form-data".equals(contentType)) { // 這裡是處理檔案上傳請求
parseParts();
success = true;
return ;
}
if(!("application/x-www-form-urlencoded".equals(contentType))) {// 這裡如果是非POST請求直接返回,不再進行處理
success = true;
return;
}
//下面的程式碼才是處理普通POST請求引數
//省略部分程式碼......
try {
if (readPostBody(formData, len)!= len) { // 讀取請求體資料
return;
}
} catch (IOException e) {
// Client disconnect
if(context.getLogger().isDebugEnabled()) {
context.getLogger().debug(
sm.getString("coyoteRequest.parseParameters"),e);
}
return;
}
parameters.processParameters(formData, 0, len); // 處理POST請求引數,把它放到requestparameter map中(即request.getParameterMap獲取到的Map,request.getParameter(name)也是從這個Map中獲取的)
// 省略部分程式碼......
}
protected int readPostBody(byte body[], int len)
throws IOException {
int offset = 0;
do {
int inputLen = getStream().read(body, offset, len - offset);
if (inputLen <= 0) {
return offset;
}
offset += inputLen;
} while ((len - offset) > 0);
return len;
}
從上面程式碼可以看出,Content-Type不是application/x-www-form-urlencoded的POST請求是不會讀取請求體資料和進行相應的引數處理的,即不會解析表單資料來放到request parameter map中。所以通過request.getParameter(name)是獲取不到的。
參見部落格:AJAX POST請求中引數以form data和request payload形式在servlet中的獲取方式
但是,request.getParameter()獲取不到引數值是在沒有使用註解@MultipartConfig或者web.xml沒有配置servlet的子元素multipart-config情況下,只要使用了註解或者配置了,就能request.getParameter()獲取到表單中普通的文字域了。
程式碼分析
上傳表單:
<form action="test_10" enctype="multipart/form-data" method="post" >
會員名稱<input type="text" name="mname" size="30"/></br>
<input type="file" name="fileName" size="30"/></br>
<input type="submit" value="提交"/>
<input type="reset" value="重置"/>
</form>
- 用io流來獲取(無@MultipartConfig,用了註解後request.getInputStream()就無法獲取輸入流了,但可以獲取各個Part的輸入流,原因不明,先留個坑)
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String name=request.getParameter("mname");
System.out.println("會員名"+name);
InputStream in=request.getInputStream();
OutputStream out=response.getOutputStream();
byte[] b=new byte[1024];
int len;
while((len=in.read(b))!=-1){
out.write(b,0,len);
}
in.close();
out.close();
}
結果:
------WebKitFormBoundary61NGudaHeAB24p96
Content-Disposition: form-data; name="mname"
1234
------WebKitFormBoundary61NGudaHeAB24p96
Content-Disposition: form-data; name="fileName"; filename="abc.txt"
Content-Type: text/plain
this is a txt
------WebKitFormBoundary61NGudaHeAB24p96--
拿到資料後就是解析、擷取的問題了,解析工作可以參考這篇部落格:用 Servlet 進行上載的原理和實現
後來太麻煩了就有了第三方工具包,比如這篇部落格common-upload的使用
再後來Servlet3.0出來增加了Part介面,上傳檔案就容易多了
2. 通過javax.servlet.http.part類來讀取
Servlet3.0提供了對檔案上傳的支援,通過@MultipartConfig標註和HttpServletRequest的兩個新方法getPart()和getParts(),開發者能夠很容易實現檔案的上傳操作。
@MultipartConfig標註主要是為了輔助Servlet3.0中HttpServletRequest提供的對上傳檔案的支援。該標註寫在Servlet類的宣告之前,一表示該Servlet希望處理的請求時multipart/form-data型別的,二是使用該註解,HttpServletRequest物件才可以得到表單資料的各個部分。另外,該標註還提供了若干屬性用於簡化對上傳檔案的處理。
@MultipartConfig標註屬性
fileSizeThershold int型 是(可選) 描述:當前資料量大於該值時,內容將被寫入檔案。
location String型 是(可選) 描述:存放生成檔案的地址
maxFileSize long型 是(可選) 描述:允許上傳的檔案最大值,預設為-1,表示沒有限制
maxRequestSize long型 是(可選) 描述:針對 multipart/form-data 請求的最大數量,預設為-1,表示沒有限制
@MultipartConfig
....
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String name=request.getParameter("mname");
System.out.println("會員名"+name);
String path="E:\\demo";
Part filePart = request.getPart("fileName");
if(filePart.getSize()>1024*1024){//上傳檔案不能超過1MB大小
filePart.delete();
}else{
path+="\\number\\"+name;
File f=new File(path);
if(!f.exists()){ //目錄不存在則建立目錄
f.mkdirs();
}
}
//
String h=filePart.getHeader("content-disposition");
System.out.println(h);
//servlet 3.1版本才能用 filePart.getSubmittedFileName();
//拼接出檔名
String fname=h.substring(h.lastIndexOf("=")+2,h.length()-1);
filePart.write(path+"\\"+fname);
}
以下是Part類常用方法:
public Part getPart(String name):返回用name指定名稱的Part物件
public Collection<Part > getParts():返回所有Part物件的集合
public InputStream getInputStream() throws IOException:返回Part物件的輸入流物件
public String getContentType():返回Part物件的內容型別
public long getSize():返回Part物件的大小
public String getName():返回Part物件的名稱
public String getHeader(String name):返回Part物件指定的MIME頭的值
public Collection< String> getHeaders(String name):返回name指定的頭 值的集合
public Collection< String> getHeaderName():返回Part物件頭名稱的集合
public void delete() throws IOException:刪除臨時檔案
public void write(String fileName) throws IOException:將Part物件寫到指定的檔案中