Commons-fileupload工具的API与开发实例解析(三)

1.3.3 MultipartStream类
MultipartStream类用来对上传的请求输入流进行解析,它是整个Apache上传组件中最复杂的类。
1.设计思想
MultipartStream类中定义了一个byte[]类型的boundary成员变量,这个成员变量用于保存图1.3中的各个数据分区之间的分隔界线,每个分区

分别代表一个表单字段的信息。图1.3中的每个分区又可以分为描述头部分和主体部分,MultipartStream类中定义了一个readHeaders()方法来

读取描述头部分的内容,MultipartStream类中定义了一个readBodyData(OutputStream output)方法来读取主体部分的内容,并将这些内容写

入到一个作为参数传入进来的输出流对象中。readBodyData方法接收的参数output对象在应用中的实际类型是DeferredFileOutputStream,这

个对象又是保存在DefaultFileItem类对象中的一个成员变量,这样,readBodyData方法就可以将一个分区的主体部分的数据写入到

DefaultFileItem类对象中。
因为图1.3中的实体内容内部的字段分隔界线是在content-type头中指定的字段分隔界线前面增加了两个减号(-)字符而形成的,而每个字段

分隔界线与它前面内容之间还进行了换行,这个换行并不属于表单字段元素的内容。所以,MultipartStream类中的成员变量boundary中存储的

字节数组并不是直接从content-type头的boundary参数中获得的字符序列,而是在boundary参数中指定的字符序列前面增加了四个字节,依次

是‘\n’、‘\r’、‘-’和‘-’。MultipartStream类中定义了一个readBoundary()方法来读取和识别各个字段之间分隔界线,有一点特殊的

是,图1.3中的第一个分隔界线前面没有回车换行符,它是无法与成员变量boundary中的数据相匹配的,所以无法调用readBoundary()方法进行

读取,而是需要进行特殊处理,其后的每个分隔界线都与boundary中的数据相匹配,可以直接调用readBoundary()方法进行读取。在本章的后

面部分,如果没有特别说明,所说的分隔界线都是指成员变量boundary中的数据内容。
RFC 1867格式规范规定了描述头和主体部分必须用一个空行进行分隔,如图1.3所示,也就是描述头和主体部分使用“\n”、“\r”、“\n”、

“\r”这四个连续的字节内容进行分隔。MultipartStream类的设计者为了简化编程,在readHeaders()方法中将“\n”、“\r”、“\n”、

“\r”这四个连续的字节内容连同描述头一起进行读取。readHeaders()方法在读取数据的过程中,当它发现第一个‘\n’、‘\r’、‘\n’、

‘\r’ 连续的字节序列时就会返回,即使主体部分正好也包含了“\n”、“\r”、“\n”、“\r”这四个连续的字节内容,但是,它们只会被

随后调用的readBodyData方法作为主体内容读取,永远不会被readHeaders()方法读取到,所以,它们不会与作为描述头和主体部分的分隔字符

序列发生冲突。
由于readHeaders()方法读取了一个分区中的主体部分前面的所有内容(包括它前面的换行),而它与下一个分区之间的分隔界线前面的换行又

包含在了成员变量boundary中,这个换行将被readBoundary()方法读取,所以,夹在readheaders()方法读取的内容和readBoundary()方法读取

的内容之间的数据全部都属于表单字段元素的内容了,因此,读取分区中的主体部分的readBodyData(OutputStream output)方法不需要进行特

别的处理,它直接将读取的数据写入到DefaultFileItem类对象中封装的DeferredFileOutputStream属性对象中即可。
2. 构造方法
MultipartStream类中的一个主要的构造方法的语法定义如下:
public (InputStream input, byte[] boundary, int bufSize)
其中,参数input是指从HttpServetRequest请求对象中获得的字节输入流对象,参数boundary是从请求消息头中获得的未经处理的分隔界线,

bufSize指定图1.10中的buffer缓冲区字节数组的长度,默认值是4096个字节。这个构造方法的源代码如下:
public MultipartStream(InputStream input, byte[] boundary, int bufSize)
{
// 初始化成员变量
this.input = input;
this.bufSize = bufSize;
this.buffer = new byte[bufSize];
this.boundary = new byte[boundary.length + 4];
this.boundaryLength = boundary.length + 4;
//buffer缓冲区中保留给下次读取的最大字节个数
this.keepRegion = boundary.length + 3;
this.boundary[0] = 0x0D; //‘\n’的16进制形式
this.boundary[1] = 0x0A; //‘\r’的16进制形式
this.boundary[2] = 0x2D; //‘-’的16进制形式
this.boundary[3] = 0x2D;
//在成员变量boundary中生成最终的分隔界线
System.arraycopy (boundary, 0, this.boundary, 4, boundary.length);

head = 0; // 成员变量,表示正在处理的这个字节在buffer中的位置指针
tail = 0; // 成员变量,表示实际读入到buffer中的字节个数
}

3. readByte方法
MultipartStream类中的readByte()方法从字节数组缓冲区buffer中读一个字节,当buffer缓冲区中没有更多的数据可读时,该方法会自动从输

入流中读取一批新的字节数据来重新填充buffer缓冲区。readByte()方法的源代码如下:
public byte readByte () throws IOException
{
// 判断是否已经读完了buffer缓冲区中的所有数据
if (head == tail)
{
head = 0;
//读入新的数据内容来填充buffer缓冲区
tail = input.read(buffer, head, bufSize);
if (tail == -1)
{
throw new IOException("No more data is available ");
}
}
return buffer[head++];// 返回当前字节,head++
}
其中,head变量是MultipartStream类中定义的一个int类型的成员变量,它用于表示正在读取的字节在buffer数组缓冲区中的位置;tail变量

也是MultipartStream类中定义的一个int类型的成员变量,它用于表示当前buffer数组缓冲区装入的实际字节内容的长度。在MultipartStream

类中主要是通过控制成员变量head的值来控制对buffer缓冲区中的数据的读取和直接跳过某段数据,通过比较head与tail变量的值了解是否需

要向buffer缓冲区中装入新的数据内容。当每次向buffer缓冲区中装入新的数据内容后,都应该调整成员变量head和tail的值。

4. arrayequals静态方法
MultipartStream类中定义了一个的arrayequals静态方法,用于比较两个字节数组中的前面一部分内容是否相等,相等返回true,否则返回

false。arrayequals方法的源代码如下,参数count指定了对字节数组中的前面几个字节内容进行比较:
public static boolean arrayequals(byte[] a, byte[] b,int count)
{
for (int i = 0; i < count; i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}

5. findByte方法
MultipartStream类中的findByte()方法从字节数组缓冲区buffer中的某个位置开始搜索一个特定的字节数据,如果找到了,则返回该字节在

buffer缓冲区中的位置,不再继续搜索,如果没有找到,则返回-1。findByte方法的源代码如下,参数pos制定了不搜索的起始位置值,value

是要搜索的字节数据:
protected int findByte(byte value,int pos)
{
for (int i = pos; i < tail; i++)
{
if (buffer[i] == value)
{
return i; // 找到该值,findByte方法返回
}
}
return - 1;
}
如果程序需要在buffer缓冲区中多次搜索某个特定的字节数据,那就可以循环调用findByte方法,只是在每次调用findByte方法时,必须不断

地改变参数pos的值,让pos的值等于上次调用findByte的返回值,直到findByte方法返回-1时为止,如图1.13所示。
图1.13
6. findSeparator方法
MultipartStream类中的findSeparator方法用于从字节数组缓冲区buffer中查找成员变量boundary中定义的分隔界线,并返回分隔界线的第一

个字节在buffer缓冲区中的位置,如果在buffer缓冲区中没有找到分隔界线,则返回-1。
findSeparator方法内部首先调用findByte方法在buffer缓冲区中搜索分隔界线boundary的第一个字节内容,如果没有找到,则说明buffer缓冲

区中没有包含分隔界线;如果findByte方法在buffer缓冲区中找到了分隔界线boundary的第一个字节内容,findSeparator方法内部接着确定该

字节及随后的字节序列是否确实是分隔界线。findSeparator方法内部循环调用findByte方法,直到找到分隔界线或者findByte方法已经查找到

了buffer缓冲区中的最后boundaryLength -1个字节。findSeparator方法内部为什么调用findByte方法查找到buffer缓冲区中的最后

boundaryLength-1个字节时就停止查找呢?这是为了解决如图1.10所示的buffer缓冲区中装入了分隔界线的部分内容的特殊情况,所以在

findSeparator()方法中不要搜索buffer缓冲区中的最后的boundaryLength -1个字节,而是把buffer缓冲区中的最后这boundaryLength -1个字

节作为保留区,在下次读取buffer缓冲区时将这些保留的字节数据重新填充到buffer缓冲区的开始部分。findSeparator方法的源代码如下:
protected int findSeparator()
{
int first;
int match = 0;
int maxpos = tail - boundaryLength;//在buffer中搜索的最大位置

for (first = head;(first <= maxpos) && (match != boundaryLength);
first++)
{
//在buffer缓冲区中寻找boundary的第一个字节
first = findByte(boundary[0], first);
/*buffer中找不到boundary[0]或者boundary[0]位于保留区中,
则可以判断buffer中不存在分隔界线*/
if (first == -1 || (first > maxpos))
{
return -1;
}
//确定随后的字节序列是否确实是分隔界线的其他字节内容
for (match = 1; match < boundaryLength; match++)
{
if (buffer[first + match] != boundary[match])
{
break;
}
}
}
// 当前buffer中找到boundary,返回第一个字节所在位置值
if (match == boundaryLength)
{
return first - 1;
}
return -1; // 当前buffer中没找到boundary,返回-1
}
图1.14中描述了findSeparator方法内部定义的各个变量的示意图。
图1.14
findSeparator方法内部的代码主要包括如下三个步骤:
(1)循环调用findByte(boundary[0], first)找到buffer缓冲区中的与boundary[0]相同的字节的位置,并将位置记录在first变量中。
(2)比较buffer缓冲区中的first后的boundaryLength-1个字节序列是否与boundary中的其他字节序列相同。如果不同,说明这个first变量指

向的字节不是分隔界线的开始字节,跳出内循环,将first变量加1后继续外循环调用findByte方法;如果相同,说明在当前缓冲区buffer中找

到了分隔界线,内循环正常结束,此时match变量的值为boundaryLength,接着执行外循环将first变量加1,然后执行外循环的条件判断,由于

match != boundaryLength条件不成立,外循环也随之结束。
(3)判断match是否等于boundaryLength,如果等于则说明找到了分隔界线,此时返回成员变量boundary的第一个字节在缓冲区buffer中位置

,由于第(2)中将first加1了,所以这里的返回值应该是first-1;如果不等,说明当前缓冲区huffer中没有分隔界线,返回-1。

7. readHeaders方法
MultipartStream类中的readHeaders方法用于读取一个分区的描述头部分,并根据DiskFileUpload类的setHeaderEncoding方法设定的字符集编

码将描述头部分转换成一个字符串返回。
在调用readHeaders方法之前时,程序已经调用了findSeparator方法找到了分隔界线和读取了分隔界线前面的内容,此时MultipartStream类中

的成员变量head指向了buffer缓冲区中的分隔界线boundary的第一个字节,程序接着应调用readBoundary方法跳过分隔界线及其随后的回车换

行两个字节,以保证在调用readHeaders方法时,成员变量head已经指向了分区的描述头的第一个字节。在readHeaders方法内部,直接循环调

用readByte方法读取字节数据,并把读到的数据存储在一个字节数组输出流中,直到读取到了连续的两次回车换行字符,就认为已经读取完了

描述头的全部内容,此时成员变量head将指向分区中的主体内容的第一个字节。readHeaders()方法的源代码如下:
public String readHeaders()throws MalformedStreamException
{
int i = 0;
//从下面的代码看来,这里定义成一个byte即可,不用定义成byte数组
byte b[] = new byte[1];
//用于临时保存描述头信息的字节数组输出流
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//对描述头部分的数据内容过大进行限制处理
int sizeMax = HEADER_PART_SIZE_MAX;
int size = 0;
while (i < 4)
{
try
{
b[0] = readByte(); }
catch (IOException e)
{
throw new MalformedStreamException("Stream ended unexpectedly " );
}
size++;
//静态常量HEADER_SEPARATOR的值为:{0x0D, 0x0A, 0x0D, 0x0A}
if (b[0] == HEADER_SEPARATOR[i])
{
i++;
}
else
{
i = 0;
}
if (size <= sizeMax)
{
baos.write(b[0]); // 将当前字节存入缓冲流
}
}
String headers = null; // 找到HEADER_SEPARATOR后,获取描述头
if (headerEncoding != null)
{
try
{
headers = baos.toString(headerEncoding);
}
catch (UnsupportedEncodingException e)
{
headers = baos.toString();
}
}
else
{
headers = baos.toString();
}
return headers;
}
readHeaders方法循环调用readByte()方法逐个读取buffer缓冲区中的字节,并将读取的字节与HEADER_SEPARATOR ={‘\n’,‘\r’,‘\n’

,‘\r’}的第一个字节进行比较,如果这个字节等于HEADER_SEPARATOR的首字节‘\n’,则循环控制因子i加1,这样,下次调用readByte()方

法读取的字节将与HEADER_SEPARATOR中的第二字节比较,如果相等,则依照这种方式比较后面的字节内容,如果连续读取到了

HEADER_SEPARATOR字节序列,则循环语句结束。readHeaders方法将读取到的每个正常字节写入到了一个字节数组输出流中,其中也包括作为描

述头与主体内容之间的分隔序列HEADER_SEPARATOR中的字节数据。由于readByte()方法会自动移动head变量的值和自动向缓冲区buffer中载入

数据,所以,readHeaders方法执行完以后,成员变量head指向分区主体部分的首字节。readHeaders方法最后将把存入字节数组输出流中的字

节数据按指定字符集编码转换成字符串并返回,该字符串就是描述头字符串。

8. readBodyData方法
MultipartStream类中的readBodyData方法用于把主体部分的数据写入到一个输出流对象中,并返回写入到输出流中的字节总数。当调用

readBodyData方法前,成员变量head已经指向了分区的主体部分的首字节,readBodyData方法调用完成后,成员变量head指向分区分隔界线的

首字节。readBodyData方法中需要调用findSeparator方法找出下一个分区分隔界线的首字节位置,才能知道这次读取的分区主体内容的结束位

置。从分区主体部分的首字节开始,直到在findSeparator方法找到的下一个分区分隔界线前的所有数据都是这个分区的主体部分的数据,

readBodyData方法需要把这些数据都写到输出流output对象中。如果findSeparator方法在buffer缓冲区中没有找到分区分隔界线,

readBodyData方法还必须向buffer缓冲区中装入新的数据内容后继续调用findSeparator方法进行处理。在向buffer缓冲区中装入新的数据内容

时,必须先将上次保留在buffer缓冲区中的内容转移进新buffer缓冲区的开始处。readBodyData方法的源代码如下,传递给readBodyData方法

的参数实际上是一个DeferredFileOutputStream类对象:
public int readBodyData(OutputStream output)
throws MalformedStreamException,IOException
{
// 用于控制循环的变量
boolean done = false;
int pad;
int pos;
int bytesRead;
// 写入到输出流中的字节个数
int total = 0;
while (!done)
{
pos = findSeparator();// 搜索分隔界线
if (pos != -1) //缓冲区buffer中包含有分隔界线
{
output.write(buffer, head, pos - head);
total += pos - head;
head = pos;//head变量跳过主体数据,指向分隔界线的首字节
done = true;// 跳出循环
}
else //缓冲区buffer中没有包含分隔界线
{
/*根据缓冲区中未被readHeaders方法读取的数据内容是否大于图1.4中的
保留区的大小,来决定保留到下一次buffer缓冲区中的字节个数
*/
if (tail - head > keepRegion)
{
pad = keepRegion;
}
else
{
pad = tail - head;
}
output.write(buffer, head, tail - head - pad);
total += tail - head - pad;//统计写入到输出流中的字节个数
/*将上一次buffer缓冲区中的未处理的数据转移到
下一次buffer缓冲区的开始位置
*/
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
head = 0; //让head变量指向缓冲区的开始位置
//向buffer缓冲区中载入新的数据
bytesRead = input.read(buffer, pad, bufSize - pad);
if (bytesRead != -1)
{
//设置buffer缓冲区中的有效字节的个数
tail = pad + bytesRead;
}
else
{
/*还没有找到分隔界线,输入流就结束了,输入流中的数据格式
显然不正确,保存缓冲区buffer中还未处理的数据后抛出异常
*/
output.write(buffer, 0, pad);
output.flush();
total += pad;
throw new MalformedStreamException
("Stream ended unexpectedly ");
}
}
}
output.flush();
return total;
}

9. discardBodyData方法
MultipartStream类中的discardBodyData方法用来跳过主体数据,它与readBodyData方法非常相似,不同之处在于readBodyData方法把数据写

入到一个输出流中,而discardBodyData方法是把数据丢弃掉。discardBodyData方法返回被丢掉的字节个数,方法调用完成后成员变量head指

向下一个分区分隔界线的首字节。MultipartStream类中定义discardBodyData这个方法,是为了忽略主体内容部分的第一个分隔界线前面的内

容,按照MIME规范,消息头和消息体之间的分隔界线前面可以有一些作为注释信息的内容,discardBodyData就是为了抛弃这些注释信息而提供

的。discardBodyData方法的源代码如下:
public int discardBodyData() throws MalformedStreamException,IOException
{
boolean done = false;
int pad;
int pos;
int bytesRead;
int total = 0;
while (!done)
{
pos = findSeparator();
if (pos != -1)
{
total += pos - head;
head = pos;
done = true;
}
else
{
if (tail - head > keepRegion)
{
pad = keepRegion;
}
else
{
pad = tail - head;
}
total += tail - head - pad;
System.arraycopy(buffer, tail - pad, buffer, 0, pad);
head = 0;
bytesRead = input.read(buffer, pad, bufSize - pad);
if (bytesRead != -1)
{
tail = pad + bytesRead;
}
else
{
total += pad;
throw new MalformedStreamException
("Stream ended unexpectedly ");
}
}
}
return total;
}
10. readBoundary方法
对于图1.3中的每一个分区的解析处理,程序首先要调用readHeaders方法读取描述头,接着要调用readBodyData(OutputStream output)读取主

体数据,这样就完成了一个分区的解析。readBodyData方法内部调用findSeparator方法找到了分隔界线,然后读取分隔界线前面的内容,此时

MultipartStream类中的成员变量head指向了buffer缓冲区中的分隔界线boundary的第一个字节。findSeparator方法只负责寻找分隔界线

boundary在缓冲区buffer中的位置,不负责从buffer缓冲区中读走分隔界线的字节数据。在调用readBodyData方法之后,程序接着应该让成员

变量head跳过分隔界线,让它指向下一个分区的描述头的第一个字节,才能调用readHeaders方法去读取下一个分区的描述头。
MultipartStream类中定义了一个readBoundary方法,用于让成员变量head跳过分隔界线,让它指向下一个分区的描述头的第一个字节。对于图

1.3中的最后的分隔界线,它比其他的分隔界线后面多了两个“-”字符,而其他分隔界线与下一个分区的内容之间还有一个回车换行,所以,

readBoundary方法内部跳过分隔界线后,还需要再读取两个字节的数据,才能让成员变量head指向下一个分区的描述头的第一个字节。

readBoundary方法内部读取分隔界线后面的两个字节数据后,根据它们是回车换行、还是两个“-”字符,来判断这个分隔界线是下一个分区的

开始标记,还是整个请求消息的实体内容的结束标记。如果readBoundary方法发现分隔界线是下一个分区的开始标记,那么它返回true,否则

返回false。readBoundary()方法的源代码如下:
public boolean readBoundary()throws MalformedStreamException
{
byte[] marker = new byte[2];
boolean nextChunk = false;
head += boundaryLength; // 跳过分隔界线符
try
{
marker[0] = readByte();
marker[1] = readByte();
// 静态常量STREAM_TERMINATOR ={‘-’、‘-’}
if (arrayequals(marker, STREAM_TERMINATOR, 2))
{
nextChunk = false;
}
// 静态常量FIELD_SEPARATOR ={‘/n’、‘/r’}
else if (arrayequals(marker, FIELD_SEPARATOR, 2))
{
nextChunk = true;
}
else
{
/*如果读到的既不是回车换行,又不是两个减号,
说明输入流有问题,则抛
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值