- commons-fileupload框架源碼解析(一)--實(shí)例
- commons-fileupload框架源碼解析(二)--HTTP
- commons-fileupload框架源碼解析(三)--ParseRequest
- commons-fileupload框架源碼解析(四)--FileItemIterator
- commons-fileupload框架源碼解析(五)--MultipartStream
- commons-fileupload框架源碼解析(六)--ParameterParser
- commons-fileupload框架源碼解析(七)--FileCleaningTracker
- commons-fileupload框架源碼解析(八)--DeferredFileOutputStream
前言
在FileItemIteratorImpl上,其實(shí)在讀取解析主體文本這一塊,是交給了MultipartStream進(jìn)行處理,F(xiàn)ileItemIteratorImpl更準(zhǔn)確的說,是將MultipartStream讀取到的內(nèi)容油猫,封裝成FileItemStream.所以要想知道commons-Fileupload是如何讀取主體文本的,就需要深入了解MultipartStream的代碼,而MultipartStream插爹,也是這個(gè)框架的中最難深入,最容易混亂,我當(dāng)時(shí)花最長的解讀時(shí)間也是在MultipartStream,深有體會(huì)赠尾,尤其是在調(diào)試一步一步的每行代碼時(shí)力穗,因?yàn)镸ultipartStream由于直接操作的是字節(jié),所以很難理解每一步在干什么气嫁,為什么要這樣做当窗。
另外,我是根據(jù)程序一步一步執(zhí)行下去的方式寸宵,遇到那個(gè)方法就解釋那個(gè)方法崖面,這樣可能會(huì)導(dǎo)致讀者的混亂,所以梯影,我建議下載這個(gè)框架源碼巫员,一邊看我的博客,一步一步的調(diào)試甲棍,遇到不懂的方法简识,在本文中搜索一下位置,我已經(jīng)盡力在代碼塊上寫上備注救军,以便理解财异,如果單純看我這篇文章,肯定是看得想吐唱遭。
HttpServletRequest.getInputStream
在解析源碼之前戳寸,我們想看看HttpServletRequest.getInputStream里面有什么內(nèi)容,因?yàn)镸ultipartStream的讀取解析工作都是對HttpServletRequest.getInputStream的內(nèi)容進(jìn)行開展的拷泽,如果不了解HttpServletRequest.getInputStream里面的內(nèi)容疫鹊,光看源碼,調(diào)試司致,是非常困難的事情拆吆。內(nèi)容我以截圖的方式弄出來
說明一下:
藍(lán)色框就是主體內(nèi)容
紅色框的就是參數(shù)內(nèi)容
------WebKitFormBoundaryuCJWrl4DtkP7RoK5就是分割線,主體文本的最后一條分割線后面會(huì)多兩個(gè)'-'
源碼
MultipartStream構(gòu)造方法
public MultipartStream(InputStream input,
byte[] boundary,
int bufSize,//默認(rèn)是4096字節(jié)
ProgressNotifier pNotifier) {
if (boundary == null) {
throw new IllegalArgumentException("boundary may not be null");
}
// We prepend CR/LF to the boundary to chop trailing CR/LF from
// body-data tokens.
this.boundaryLength = boundary.length + BOUNDARY_PREFIX.length;//各個(gè)數(shù)據(jù)內(nèi)容之間的分隔界線的組成: CR+LF+兩個(gè)'-'+從消息頭Content-Type找到boundary的值
//因?yàn)樵诓檎曳指罱泳€的操作是在緩存區(qū)buffer中進(jìn)行查找到脂矫,所以bufSize必須起碼能裝得分割線的所有字節(jié)
if (bufSize < this.boundaryLength + 1) {
throw new IllegalArgumentException(
"The buffer size specified for the MultipartStream is too small");
}
this.input = input;//HttpServletRequest的流枣耀,流的數(shù)據(jù)就是主體文本
this.bufSize = Math.max(bufSize, boundaryLength * 2);//框架認(rèn)為bufSize越大越好,畢竟這個(gè)讀取主體內(nèi)容的速度庭再,和解析捞奕、查詢效率
this.buffer = new byte[this.bufSize];
this.notifier = pNotifier;
this.boundary = new byte[this.boundaryLength];
this.boundaryTable = new int[this.boundaryLength + 1];//KMP算法的前綴表
this.keepRegion = this.boundary.length;//在buffer中預(yù)留的分割線字節(jié)數(shù)
//將成員變量boundary賦值,首先將BOUNDARY_PREFIX的元素復(fù)制到成員變量boundary的開頭拄轻,再將參數(shù)boundary的元素復(fù)制成員變量boundary剩下的元素
//即\r\n------WebKitFormBoundary7MA4YWxkTrZu0gW
System.arraycopy(BOUNDARY_PREFIX, 0, this.boundary, 0,
BOUNDARY_PREFIX.length);
System.arraycopy(boundary, 0, this.boundary, BOUNDARY_PREFIX.length,
boundary.length);
computeBoundaryTable();//計(jì)算出分割線的KMP算法前綴表
head = 0;//讀取buffer的開始位置颅围,0<=head<=buffer.length
tail = 0;//讀取buffer的結(jié)束位置,0<=head<=buffer.length
}
這里就是對需要用到的成員變量進(jìn)行初始化,值得注意的是恨搓,MultipartStream在buffer中查詢分割線boundary是采用KMP算法院促,不算KMP算法的讀者筏养,請先去了解一下KMP算法。computeBoundaryTable()就是用于計(jì)算KMP算法所需的boundary前綴表
SkipPreamble
在開始讀取主體文本的第一步是調(diào)用skipPreamble方法常拓,參照UploadFileBase.parseRequest(RequestContext)
public boolean skipPreamble() throws IOException {
// First delimiter may be not preceeded with a CRLF.
//去掉前面的‘\n’和‘\r’這兩個(gè)字節(jié)渐溶,因?yàn)榈谝粋€(gè)分割線前面沒有回車換行符,無法與成員變量boundary中的數(shù)據(jù)相匹配
System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2);
boundaryLength = boundary.length - 2;
computeBoundaryTable();//計(jì)算出分割線的KMP算法前綴表
try {
// Discard all data up to the delimiter.
//丟掉第一個(gè)分隔界線符前數(shù)據(jù)弄抬,一般是沒有的掌猛,如果有就是一些注釋數(shù)據(jù)的數(shù)據(jù)
// 按照MIME規(guī)范,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內(nèi)容
discardBodyData();
// Read boundary - if succeeded, the stream contains an
// encapsulation.
return readBoundary();
} catch (MalformedStreamException e) {
return false;
} finally {
// Restore delimiter.
//成員變量boundary恢復(fù)回原來的樣子眉睹,并重新計(jì)算成員變量boundary的KMP算法前綴表
System.arraycopy(boundary, 0, boundary, 2, boundary.length - 2);
boundaryLength = boundary.length;
boundary[0] = CR;
boundary[1] = LF;
computeBoundaryTable();//計(jì)算出分割線的KMP算法前綴表
}
}
在這里荔茬,我先不急著去說明skipPreamble的作用,而是先進(jìn)入discardBodyData()看看
discardBodyData
public int discardBodyData() throws MalformedStreamException, IOException {
return readBodyData(null);//
}
從方法名中竹海,可以看出該方法用于丟棄沒有的數(shù)據(jù)慕蔚,如,按 照MIME規(guī)范斋配,消息頭和消息體之間的分隔界線前面可以有一些作為注釋信息的內(nèi)容孔飒。其實(shí)現(xiàn)的方式是調(diào)用readBodyData(OutputStream)。
readBodyData(OutputStream)
public int readBodyData(OutputStream output)
throws MalformedStreamException, IOException {
//Streams.copy(InputStrea,OutputStream,closeOuputStream):將輸入流數(shù)據(jù)傳到輸出流中艰争,如果輸入流為null坏瞄,
// 則只進(jìn)行對輸入流的讀取,而不會(huì)講讀取的數(shù)據(jù)傳給輸出流,closeOuptStream表示是否關(guān)閉輸出流
// 這返回已經(jīng)寫入的字節(jié)數(shù)甩卓,如果傳入的是輸出流為null的話鸠匀,則這里表示已經(jīng)讀取的字節(jié)數(shù)
return (int) Streams.copy(newInputStream(), output, false); // N.B. Streams.copy closes the input stream
}
這里要注意一下,readBodyData返回的是已經(jīng)寫入的字節(jié)數(shù)逾柿,但是如果傳入的輸出流為null缀棍,則返回已經(jīng)讀取的字節(jié)數(shù)。事實(shí)上机错,從源碼可以看到爬范,Streams.copy計(jì)算數(shù)量是在對每次輸入流讀取的字節(jié)數(shù)累加的,而不是對每次輸輸出流輸出的字節(jié)數(shù)累加弱匪,所以青瀑,Streams.copy更準(zhǔn)確的說返回的是已經(jīng)讀取的字節(jié)數(shù)。
在readBoyData中萧诫,我們看到調(diào)用了newInputStream()
newInputStream
ItemInputStream newInputStream() {
return new ItemInputStream();
}
又是內(nèi)部類斥难,交給ItemInputStream,ItemInputStream是繼承了InputStream的MultipartStream內(nèi)部了财搁,實(shí)現(xiàn)讀取主體文本中的參數(shù)內(nèi)容的相關(guān)操作蘸炸,這里我需要先說明一下ItemInputStream是如何讀取主體文本中的參數(shù)內(nèi)容躬络。
ItemInputStream讀取參數(shù)內(nèi)容的操作解析
從圖1可以看出尖奔,主體文本的組成就是分割線+參數(shù)內(nèi)容,所以我們要準(zhǔn)確地讀取到參數(shù)內(nèi)容,需要確定好分割線的起始位置提茁,分割線的長度淹禾。但是在讀取過程,因?yàn)橹黧w文本一般都比較大茴扁,只能通過一個(gè)緩存區(qū)buffer來分段讀取铃岔,也就造成了一個(gè)問題,無法確定那里是分割線峭火,因?yàn)閎uffer有可能完全將分割線的字節(jié)讀取了毁习,也有可能只讀取了分割線的部分字節(jié),也有可能完全沒有讀取到分割線的字節(jié)卖丸,所以MultipartStream定義了三個(gè)成員變量:buffer讀取的開始位置head,buffer讀取的最后一個(gè)位置tail,還有保留分割線字節(jié)數(shù)pad纺且,在讀取buffer的數(shù)據(jù)時(shí),以head作為開始讀取的游標(biāo)稍浆,以tail做為讀取buffer的結(jié)束位置载碌,而pad,會(huì)在buffer中沒有找到分割線時(shí)候衅枫,保留buffer的最后pad個(gè)的字節(jié)嫁艇,在下一次的從主體內(nèi)容的流中讀取到buffer的時(shí)候,會(huì)先將原buffer中最后pad個(gè)字節(jié)放到buffer的前面弦撩,再填充剩下的buffer字節(jié)步咪,從而精確的讀到參數(shù)內(nèi)容。
具體實(shí)現(xiàn)益楼,來看源碼:
ItemInputStream構(gòu)造方法
ItemInputStream() {
findSeparator();
}
調(diào)用了查詢分割線的方法歧斟,我們再進(jìn)去看看
ItemInputStream.findSeparator()
private void findSeparator() {
pos = MultipartStream.this.findSeparator();
if (pos == -1) {//緩沖區(qū)buffer中沒有包含分隔界線
//讀取的數(shù)據(jù)量是否大于保留區(qū)的大小,來決定保留到下一次buffer緩沖區(qū)中的字節(jié)個(gè)數(shù)
if (tail - head > keepRegion) {
pad = keepRegion;//大于保留區(qū)偏形,取保留區(qū)大小
} else {
pad = tail - head;//小于保留區(qū)静袖,取所有數(shù)據(jù)量
}
}
}
當(dāng)在buffer中,沒有找到分割線俊扭,pad將會(huì)被賦值队橙,keepRegion就是分割線的字節(jié)數(shù)。來看看MutlitpartStream.findSeparator的實(shí)現(xiàn)
protected int findSeparator() {
int bufferPos = this.head;
int tablePos = 0;
//KMP算法
while (bufferPos < this.tail) {
while (tablePos >= 0 && buffer[bufferPos] != boundary[tablePos]) {
tablePos = boundaryTable[tablePos];
}
bufferPos++;
tablePos++;
if (tablePos == boundaryLength) {
return bufferPos - boundaryLength;//通過減去分割線的長度就能得到分割線在buffer的起始位置
}
}
return -1;
}
可以看到萨惑,是buffer中進(jìn)行查找分割線捐康,而查詢的起始位置是head,使用的查詢方法是KMP算法庸蔼,而返回的就是分割線的在buffer中的起始位置
ItemInputStream.read(Byte[],int,int)
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (closed) {//判斷流是否已經(jīng)關(guān)閉
throw new FileItemStream.ItemSkippedException();
}
if (len == 0) {
return 0;
}
int res = available();//返回有效字節(jié)數(shù)
if (res == 0) {
res = makeAvailable();//讀取更多字節(jié)到buffer中解总,并返回有效字節(jié)數(shù)
if (res == 0) {
return -1;
}
}
res = Math.min(res, len);
System.arraycopy(buffer, head, b, off, res);
head += res;
total += res;
return res;
}
先用available來查看buffer有沒有數(shù)據(jù)可以讀取,如果沒有通過makeAvailable從主體文本inputStream中讀取進(jìn)buffer中姐仅。我們先進(jìn)去看看available
public int available() throws IOException {
if (pos == -1) {//未找到分割線
//buffer中的有效字節(jié)數(shù)=buffer中可讀的最后一個(gè)字節(jié)位置-buffer中已讀的字節(jié)數(shù)head-減去去保留區(qū)后的數(shù)據(jù)量pad
return tail - head - pad;
}
//找到分割線的情況下:buffer中的有效字節(jié)數(shù)=buffer中分割線的開始位置-buffer中已讀的字節(jié)數(shù)head
return pos - head;//返回分割線前面的數(shù)據(jù)量
}
該方法就是返回在buffer中還可以讀的字節(jié)數(shù)花枫,即buffer中的有效字節(jié)數(shù)刻盐。
ItemInputStream.makeAvailable
private int makeAvailable() throws IOException {
if (pos != -1) {
return 0;//在buffer中找到了分割線,就意味著已經(jīng)讀取完一個(gè)參數(shù)內(nèi)容劳翰,所以沒必要讀取下去敦锌。
}
// Move the data to the beginning of the buffer.
//total 是統(tǒng)計(jì)已處理的字節(jié)數(shù)
// tail-head-pad一般情況下都會(huì)是得出的結(jié)果都是0,因?yàn)閔ead在調(diào)用read的相關(guān)方法的時(shí)候會(huì)累加已讀去的字節(jié)數(shù)佳簸,使得head越來越逼近tail
// 當(dāng)時(shí)又不會(huì)超過tail-pad的范圍
total += tail - head - pad;
//將上一次buffer緩沖區(qū)中的未處理的數(shù)據(jù)轉(zhuǎn)移到下一次buffer緩沖區(qū)的開始位置
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
// Refill buffer with new data.
head = 0;
tail = pad;//因?yàn)檫@個(gè)時(shí)候的buffer已經(jīng)將上一次pad個(gè)字節(jié)放到buffer的開頭乙墙,所以tail要從pad開始,再通過下面的循環(huán)累加下去生均。
for (;;) {
int bytesRead = input.read(buffer, tail, bufSize - tail);//再讀取buffer緩沖區(qū)剩下的數(shù)據(jù)量
if (bytesRead == -1) {
// The last pad amount is left in the buffer.
// Boundary can't be in there so signal an error
// condition.
final String msg = "Stream ended unexpectedly";
throw new MalformedStreamException(msg);
}
if (notifier != null) {
notifier.noteBytesRead(bytesRead);
}
tail += bytesRead;
findSeparator();//再次查找看看有沒有分割線在buffer中,并確定保留的字節(jié)數(shù)pad的值
int av = available();
if (av > 0 || pos != -1) {
return av;//返回有效的字節(jié)數(shù)
}
}
}
該方法就是從主體文本中讀取剩下的字節(jié)給buffer重新填充听想,填充之前,會(huì)先將上一次buffer的保留字節(jié)數(shù)挪到這次buffer的前面马胧,然后將讀取主體文本的字節(jié)填充到buffer剩下的元素哗魂。該方法還會(huì)調(diào)用findSeparator確定好分割線的開始位置,并返回調(diào)用available返回有效的字節(jié)數(shù)漓雅。
readBoundary
回到MultipartStream.skipPreamble方法上录别,除了調(diào)用discardBodayDat之外,還調(diào)用了readBoundary
public boolean readBoundary()
throws FileUploadIOException, MalformedStreamException {
byte[] marker = new byte[2];
boolean nextChunk = false;
head += boundaryLength;//通過對head游標(biāo)加上分割線長度邻吞,使得head游標(biāo)跳過分割線的數(shù)據(jù)组题,
try {
//雖然已經(jīng)跳過了分割線的數(shù)據(jù)字節(jié),但是分割線后面還跟著一個(gè)回車換行抱冷,通過readByte逐個(gè)讀取處理
//然后判斷是否真的是回車換行還是兩個(gè)'-'崔列,
// 如果是回車換行,表示分隔界線是下一個(gè)分區(qū)的開始標(biāo)記旺遮,返回true
// 如果是兩個(gè)'-',返回false赵讯,表示已經(jīng)到了文本體的末尾
marker[0] = readByte();
if (marker[0] == LF) {
// Work around IE5 Mac bug with input type=image.
// Because the boundary delimiter, not including the trailing
// CRLF, must not appear within any file (RFC 2046, section
// 5.1.1), we know the missing CR is due to a buggy browser
// rather than a file containing something similar to a
// boundary.
//IE5 Mac 的bug。
return true;
}
marker[1] = readByte();
if (arrayequals(marker, STREAM_TERMINATOR, 2)) {
nextChunk = false;
} else if (arrayequals(marker, FIELD_SEPARATOR, 2)) {
nextChunk = true;
} else {
throw new MalformedStreamException(
"Unexpected characters follow a boundary");
}
} catch (FileUploadIOException e) {
// wraps a SizeException, re-throw as it will be unwrapped later
throw e;
} catch (IOException e) {
throw new MalformedStreamException("Stream ended unexpectedly");
}
return nextChunk;
}
通過head游標(biāo)跳過分割線的數(shù)據(jù)和分割線與下個(gè)參數(shù)內(nèi)容的數(shù)據(jù)開始位置的回車換行耿眉,還檢驗(yàn)是否存在下一個(gè)分區(qū)數(shù)據(jù)边翼,和是否已經(jīng)到文本體結(jié)尾
并返回是否存在下一個(gè)參數(shù)內(nèi)容,返回true存在下一個(gè)分區(qū)數(shù)據(jù)鸣剪,返回false组底,說明已經(jīng)到文本體結(jié)尾
public byte readByte() throws IOException {
// Buffer depleted ?//
if (head == tail) {//這個(gè)情況一般會(huì)出現(xiàn)在一開始讀取數(shù)據(jù)的時(shí)候,head==0,tail==0
head = 0;
// Refill.
tail = input.read(buffer, head, bufSize);//標(biāo)記1
if (tail == -1) {
// No more data available.
throw new IOException("No more data is available");
}
if (notifier != null) {
notifier.noteBytesRead(tail);
}
}
return buffer[head++];
}
一般情況下調(diào)用skipPremble的時(shí)候筐骇,也只有一開始的讀取主體文本的時(shí)候债鸡,head==tail才會(huì)成立,也就是說铛纬,readByte才是真正開始讀取主體文本數(shù)據(jù)的方法厌均。該方法一般情況下,都是直接通過head++直接取出buffer的時(shí)候告唆,還有一點(diǎn)需要注意的是棺弊,標(biāo)記1的位置晶密,input是HttpServletRequest的主體文本的字節(jié)流InputStream,而不是內(nèi)部ItemInputStream镊屎。
readHeaders
public String readHeaders() throws FileUploadIOException, MalformedStreamException {
int i = 0;
byte b;
// to support multi-byte characters
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int size = 0;
//該循環(huán),會(huì)讀取完該分區(qū)的所有消息頭數(shù)據(jù)字節(jié)直到讀取完兩個(gè)回車換行結(jié)束
while (i < HEADER_SEPARATOR.length) {
try {
b = readByte();
} catch (FileUploadIOException e) {
// wraps a SizeException, re-throw as it will be unwrapped later
throw e;
} catch (IOException e) {
throw new MalformedStreamException("Stream ended unexpectedly");
}
if (++size > HEADER_PART_SIZE_MAX) {
throw new MalformedStreamException(
format("Header section has more than %s bytes (maybe it is not properly terminated)",
Integer.valueOf(HEADER_PART_SIZE_MAX)));
}
if (b == HEADER_SEPARATOR[i]) {
i++;
} else {
i = 0;
}
baos.write(b);
}
//將獲取到的字節(jié)根據(jù)字符串headerEncoding轉(zhuǎn)換成字符串
String headers = null;
if (headerEncoding != null) {
try {
headers = baos.toString(headerEncoding);
} catch (UnsupportedEncodingException e) {
// Fall back to platform default if specified encoding is not
// supported.
headers = baos.toString();
}
} else {
headers = baos.toString();
}
return headers;
}
讀取分區(qū)里的消息頭部分,返回消息頭以及消息頭與值之間的兩個(gè)回車換行茄螃,即:Content-Disposition: form-data; name="test"\r\n\r\n