Servlet 是用 Java 編寫的、協議和平臺都獨立的服務器端組件,使用請求/響應的模式,提供了一個基于 Java 的服務器解決方案。使用 Servlet 可以方便地處理在 HTML 頁面表單中提交的數據,但 Servlet 的 API 沒有提供對以 mutilpart/form-data 形式編碼的表單進行解碼的支持,因而對日常應用中經常涉及到到文件上傳等事務無能為力。本文將從文件傳輸的基本原理入手,分析如何用 Servlet 進行文件的上傳,并提出解決方案。
一、基本原理
通過 HTML 上載文件的基本流程如下圖所示。瀏覽器端提供了供用戶選擇提交內容的界面(通常是一個表單),在用戶提交請求后,將文件數據和其他表單信息編碼并上傳至服務器端,服務器端(通常是一個 cgi 程序)將上傳的內容進行解 碼了,提取出 HTML 表單中的信息,將文件數據存入磁盤或數據庫。

二、各過程詳解
A)填寫表單并提交
通過表單提交數據的方法有兩種,一種是 GET 方法,另一種是 POST 方法,前者通常用于提交少量的數據,而在上傳文件或大量數據時,應該選用 POST 方法。在 HTML 代碼中,在 <form> 標簽中添加以下代碼可以頁面上顯示一個選擇文件的控件。
<input type="file" name="file01">
可以直接在文本框中輸入文件名,也可以點擊按鈕后彈出供用戶選擇文件的對話框。
B)瀏覽器編碼
在向服務器端提交請求時,瀏覽器需要將大量的數據一同提交給 Server 端, 而提交前,瀏覽器需要按照 Server 端可以識別的方式進行編碼,對于普通的表單數據,這種編碼方式很簡單,編碼后的結果通常是 field1=value2&field2=value2&… 的形式,如 name=aaaa&Submit=Submit。這種編碼的具體規則可以在 rfc2231 里查到, 通常使用的表單也是采用這種方式編碼的,Servlet 的 API 提供了對這種 編碼方式解碼的支持,只需要調用 ServletRequest 類中的方法就可以得到 用戶表單中的字段和數據。
這種編碼方式( application/x-www-form-urlencoded )雖然簡單,但對于傳輸大塊的二進制數據顯得力不從心,對于傳輸這類數據,瀏覽器采用了另一種編碼方式,即 "multipart/form-data" 的編碼方式,采用這種方式,瀏覽器可以很容易的表單內的數據和文件一起。這種編碼方式先定義好一個不可能在數據中出現的字符串作為分界符,然后用它將各個數據段分開,而對于每個數據段都對應著 HTML 頁面表單中的一個 Input 區,包括一個 content-disposition 屬性,說明了這個數據段的一些信息,如果這個數據段的內容是一個文件,還會有 Content-Type 屬性,然后就是數據本身。 這里,我們可以編寫一個簡單的 Servlet 來看到瀏覽器到底是怎樣編碼的。
實現流程:
- 重載 HttpServlet 中的 doPost 方法
- 調用 request.getContentLength() 得到 Content-Length ,并定義一個與 Content-Length 大小相等的字節數組 buffer 。
- 從HttpServletRequest 的實例 request 中得到一個 InputStream, 并把它讀入 buffer 中。
- 使用 FileOutputStream 將 buffer 寫入指定文件。
代碼清單
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class ReceiveServlet extends HttpServlet
{

private static final long serialVersionUID = 1L;

public void doGet(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException
{
doPost(request, response);
}

public void doPost(HttpServletRequest request, HttpServletResponse response)

throws ServletException, IOException
{
// 1
int len = request.getContentLength();
byte buffer[] = new byte[len];
// 2
InputStream in = request.getInputStream();
int total = 0;
int once = 0;

while ((total < len) && (once >= 0))
{
once = in.read(buffer, total, len);
total += once;
}
// 3
OutputStream out = new BufferedOutputStream(new FileOutputStream(
"E:/Receive.log", true));
byte[] breaker = "\r\nNewLog: -------------------->\r\n".getBytes();
System.out.println(request.getContentType());
out.write(breaker, 0, breaker.length);
out.write(buffer);
out.close();
}
}
在使用IE作為瀏覽器測試時,從指定的文件( Receive.log
)中可以看到如下的內容
NewLog: -------------------->
-----------------------------7d802110e4c
Content-Disposition: form-data; name="file01"; filename=""
Content-Type: application/octet-stream


-----------------------------7d802110e4c--
這里 ----------------------------7d802110e4c 作為分界符。關于分界符的規則可以概況為兩條:
- 除了最后一個分界符,每個分界符后面都加一個 CRLF 即 '\u000D' 和 '\u000A', 最后一個分界符后面是兩個分隔符"--"
- 每個分界符的開頭也要加一個 CRLF 和兩個分隔符("-")。
瀏覽器采用默認的編碼方式是 application/x-www-form-urlencoded ,可以通過指定 form 標簽中的 enctype 屬性使瀏覽器知道此表單是用 multipart/form-data 方式編碼如:
< form action="ReceiveServlet.do" ENCTYPE="multipart/form-data" method=post >
C)提交請求
提交請求的過程由瀏覽器完成的,并且遵循 HTTP 協議,每一個從瀏覽器端到服務器端的一個請求,都包含了大量與該請求有關的信息, 在 Servlet 中,HttpServletRequest 類將這些信息封裝起來,便于我們提取使用。在文件上載和表單提交的過程中,有兩個指的關心的問題,一是上載的數據是是采用的那種方式的編碼,這個問題的可以從 Content-Type 中得到答案,另一個是問題是上載的數據量有多少即 Content-Length ,知道了它,就知道了 HttpServletRequest 的實例中有多少數據可以讀取出來。這兩個屬性,我們都可以直接從 HttpServletRequest 的一個實例中獲得,具體調用的方法是 getContentType() 和 getContentLength() 。
Content-Type 是一個字符串,在上面的例子中,增加
System.out.println(request.getContentType());
可以得到這樣的一個輸出字符串:
multipart/form-data; boundary=---------------------------7d802110e4c
前半段正是編碼方式,而后半段正是分界符,通過 String 類中的方法,我們可以把這個字符串分解,提取出分界符。
String contentType=request.getContentType();
int start=contentType.indexOf("boundary=");
int boundaryLen=new String("boundary=").length();
String boundary=contentType.substring(start+boundaryLen);
boundary="--"+boundary;

判斷編碼方式可以直接用 String 類中的 startsWith 方法判斷。
if(contentType==null || !contentType.startsWith("multipart/form-data"))

這樣,我們在解碼前可以知道:
編碼的方式是否是multipart/form-data
數據內容的分界符
數據的長度
我們可以用類似于 ReceiveServlet 中的方式將這個請求的輸入流讀入一個長度為 Content-Length 的字節數組,接下來就是將這個字節數組里的內容全部提取出來了。
D)解碼
解碼對我們來說是整個上載過程最繁瑣的一個步驟,經過以上的流程,我們可以得到一個包含有所有上載數據的一個字節數組和一個分界符,通過對 Receive.log 分析,還可以得到每個數據段中的分界符。而我們要得到以下內容:
- 提交的表單中的各個字段以及對應的值
- 如果表單中有 file 控件,并且用戶選擇了上載文件,則需要分析出字段的名稱、文件在瀏覽器端的名字、文件的 Content-Type 和文件的內容。
字節數組的內容可以分解如下:

具體解碼過程也可以分為兩個步驟:
- 將上載的數據分解成數據段,每個數據段對應著表單中的一個 Input 區。
- 對每個數據段,再進行分解,提出上述要求得到的內容。
這兩個步驟主要的操作有兩個,一個是從一個數組中找出另一個數組的位置,類似于 String 類中的 indexOf 的功能,另一個是從一個數組中提取出另一個數組, 類似于 String 類中的 substring 的功能,為此我們可以專門寫兩個方法,實現這種功能。
int byteIndexOf (byte[] source,byte[] search,int start)
byte[] subBytes(byte[] source,int from,int end)

為了便于使用,可以從這兩個方法中衍生出下列方法
int byteIndexOf (byte[] source,String search,int start) 以一個 String 作為搜索對象參數
String subBytesString(byte[] source,int from,int end) 直接返回一個 String
int bytesLen(String s) 返回字符串轉化為字節數組后,字節數組的長度

這樣,從一個字節數組中,根據標記提取出另一個字節數組可以表示如下:

假設我們已經將數據存入字節數組 buffer 中,分界符存入 String boundary 中
int pos1=0; //pos1 記錄 在buffer 中下一個 boundary 的位置
//pos0,pos1 用于 subBytes 的兩個參數
int pos0=byteIndexOf(buffer,boundary,0);
//pos0 記錄 boundary 的第一個字節在buffer 中的位置
do

{
pos0+=boundaryLen;
//記錄boundary后面第一個字節的下標
pos1=byteIndexOf(buffer,boundary,pos0);
if (pos1==-1)
break;
pos0+=2; //考慮到boundary后面的 \r\n
PARSE[(subBytes(buffer,pos0,pos1-2));]
//考慮到boundary后面的 \r\n
pos0=pos1;
}while(true);

其中 PARSE 部分是對每一個數據段進行解碼的方法,考慮到 Content-Disposition 等屬性,首先定義一個 String 數組

String[] tokens=
{"name=\"",
"\"; filename=\"",
"\"\r\n",
"Content-Type: ",
"\r\n\r\n"
};

對于一個不是文件的數據段,只可能有 tokens 中的第一個元素和最后一個元素,如果是一個文件數據段,則包含所有的元素。第一步先得到 tokens 中每個元素在這個數據段中的位置
int[] position=new int[tokens.length];
for (int i=0;i < tokens.length ;i++ )

{
position[i]=byteIndexOf(buffer,tokens[i],0);
}

第二步判斷是否是一個文件數據段,如果是一個文件 數據段則 position[1] 應該大于0,并且 postion[1] 應該小于 postion[2] 即 position[1] > 0 && position[1] < position[2] 如果為真,則為一個文件數據段,
1.得到字段名
String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[1]);
2.得到文件名
String file= subBytesString(buffer,position[1]+bytesLen(tokens[1]),position[2]);
3.得到 Content-Type
String contentType=subBytesString(buffer,position[3]+bytesLen(tokens[3]),position[4]);
4.得到文件內容
byte[] b=subBytes(buffer,position[4]+bytesLen(tokens[4]),buffer.length);
否則,說明數據段是一個 name/value 型的數據段,
且name 在 tokens[0] 和 tokens[2] 之間,value 在 tokens[4]之后
//1.得到 name
String name =subBytesString(buffer,position[0]+bytesLen(tokens[0]),position[2]);
//2.得到 value
String value= subBytesString(buffer,position[4]+bytesLen(tokens[4]),buffer.length);

三、具體實現
為便于使用,定義 upload 包,包括以下類:
ContentFactory
對從 client 中傳來的數據進行解碼,并提供一系列 get 方法,從中得到上傳的各種信息。
具體接口如下
staticContentFactory |
getContentFactory (javax.servlet.http.HttpServletRequestrequest)
返回根據當前請求生成的一個 ContentFactory 實例 |
staticContentFactory |
getContentFactory (javax.servlet.http.HttpServletRequestrequest, intmaxLength)
返回根據當前請求生成的一個 ContentFactory 實例 |
FileHolder |
getFileParameter (java.lang.Stringname)
返回一個 FileHolder 實例,該實例包含了通過字段名為 name 的 file 控件上載的文件信息,如果不存在這個字段或者提交頁面時,沒有選擇上載的文件,則返回 null。 |
java.util.Enumeration |
getFileParameterNames ()
返回一個 由 String 對象構成的 Enumeration ,包含了 Html 頁面窗體中所有 file 控件的 name 屬性。 |
FileHolder[] |
getFileParameterValues (java.lang.Stringname)
返回一個 FileHolder 數組,該數組包含了所有通過字段名為 name 的 file 控件上載的文件信息,如果不存在這個字段或者提交頁面時,沒有選擇任何上載的文件,則返回一個零元素的數組(不是 null )。 |
java.lang.String |
getParameter (java.lang.Stringname)
以 String 類型返回請求的參數的值,如果該參數不存在,則返回為 null 。參數存于提交的表單數據中。 |
java.util.Enumeration |
getParameterNames ()
返回一個 String 類型的 Enumeration 對象,該對象包含了所有提交請求的參數名稱。 |
java.lang.String[] |
getParameterValues (java.lang.Stringname)
返回 String 類型的數組,該數組包含了指定名稱的參數對應的所有的值,如果參數不存在,則返回為 null 。 |
FileHolder
封裝一個文件數據段,可以從中提取文件名, Content-Type 和文件內容等屬性。 接口如下:
byte[] |
getBytes ()
返回一個文件內容的字節數組 |
java.lang.String |
getContentType () 返回該文件的 Content-Type |
java.lang.String |
getFileName ()
返回該文件在文件上載前在客戶端的名稱 |
java.lang.String |
getParameterName ()
返回上載該文件時,Html 頁面窗體中 file 控件的 name 屬性 |
void |
saveTo (java.io.Filefile)
把文件的內容存到指定的文件中 |
void |
saveTo (java.lang.Stringname)
把文件的內容存到指定的文件中 |
ContentFactoryException
在 ContentFactory.getContentFactory 方法中可能拋出。
代碼:ServletUpload.rar
posted on 2008-12-31 13:18
ゞ沉默是金ゞ 閱讀(1856)
評論(2) 編輯 收藏 所屬分類:
Java EE