FileUpload 是 Apache commons下面的一个子项目,用来实现Java环境下面的文件上传功能,与常见的SmartUpload齐名。
上传我们一般就直接使用现成的工具来实现就好了,很多工具非常好用,如果
理解了它的原理,不仅有助于选择不同的工具,还有助于处理遇到的问题,或者改进工具,或者使用工具中不知道的其它功能。
本文章重点介绍FileUpload源码中几要的几个类的协作关系,主要是内部iterator类,流的数据处理方式,以及一个输入流依次分成几股后,依次分别写入不同的输出流的过程。掌握些套路,感受下作者是设计思想。
一、 先说说基本的上传过程(熟悉的可以略过)
1.客户端:
客户端代码是一个form,类型enctype用multipart/form-data,这样可以把文件中的数据作为流式数据上传,不管是什么文件类型,均可上传。
class="java" name="code"><form action="doUpload.jsp" method="post" enctype="multipart/form-data">
上传的文件:<input type="file" name="upfile" size="50">
<input type="submit" value="提交">
</form>
浏览器会把相应的数据按请求的格式发送到web服务上的,而web服务器可以把请求转成Request对象。
请求的结构示例如下:关注【boundary】,用来对请求体分块的一个字符串。
POST /t2/upload.do HTTP/1.1
User-Agent: SOHUWapRebot
Accept-Language: zh-cn,zh;q=0.5
Accept-Charset: GBK,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Content-Length: 60408
Content-Type:multipart/form-data; boundary=ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Host: w.sohu.com
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="desc"
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[......][......][......][......]...........................
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC
Content-Disposition: form-data;name="pic"; filename="photo.jpg"
Content-Type: application/octet-stream
Content-Transfer-Encoding: binary
[图片二进制数据]
--ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC--
2.服务器端:
对于服务器端的Request的处理,先举一个简单的处理过程。
String contentType = request.getContentType();
if(contentType.indexOf("multipart/form-data") >= 0){//【读入上传的数据,只处理文件类型表单】
in = new DataInputStream(request.getInputStream());
int formDataLength = request.getContentLength();
if(formDataLength > MAX_SIZE){
out.println("<P>上传的文件字节数不可以超过" + MAX_SIZE + "</p>");
return;
}
//保存上传文件的数据
byte dataBytes[] = new byte[formDataLength];//【表单中的数据的长度!!!】
int byteRead = 0;
int totalBytesRead = 0;
//上传的数据保存在byte数组
while(totalBytesRead < formDataLength){
byteRead = in.read(dataBytes,totalBytesRead,formDataLength);
totalBytesRead += byteRead;
}
//根据byte数组创建字符串
String file = new String(dataBytes);【把所有的表单流内容转成String来处理,太大估计有问题了!!!】
//out.println(file);
//取得上传的数据的文件名
String saveFile = file.substring(file.indexOf("filename=\"") + 10);
saveFile = saveFile.substring(0,saveFile.indexOf("\n"));
saveFile = saveFile.substring(saveFile.lastIndexOf("\\") + 1,saveFile.indexOf("\""));
int lastIndex = contentType.lastIndexOf("=");
//【取得数据的分隔字符串,这个分界线很重要】
String boundary = contentType.substring(lastIndex + 1,contentType.length());
//创建保存路径的文件名
String fileName = rootPath + saveFile;
int pos;
pos = file.indexOf("filename=\"");
pos = file.indexOf("\n",pos) + 1;
pos = file.indexOf("\n",pos) + 1;
pos = file.indexOf("\n",pos) + 1;
int boundaryLocation = file.indexOf(boundary,pos) - 4;
//out.println(boundaryLocation);
//取得文件数据的开始的位置
int startPos = ((file.substring(0,pos)).getBytes()).length;
//out.println(startPos);
//取得文件数据的结束的位置
int endPos = ((file.substring(0,boundaryLocation)).getBytes()).length;
//out.println(endPos);
//检查上载文件是否存在
File checkFile = new File(fileName);
if(checkFile.exists()){
out.println("<p>" + saveFile + "文件已经存在.</p>");
}
//检查上载文件的目录是否存在
File fileDir = new File(rootPath);
if(!fileDir.exists()){
fileDir.mkdirs();
}
//创建文件的写出类
fileOut = new FileOutputStream(fileName);【准备把请求中的内容写入这个文件】
//保存文件的数据
fileOut.write(dataBytes,startPos,(endPos - startPos));【表单中的byte[]数据选择头尾位置写入文件中去!!!!!】
fileOut.close();
out.println(saveFile + "文件成功上载.</p>");
}else{
String content = request.getContentType();
out.println("<p>上传的数据类型不是multipart/form-data</p>");
}
}
二、FileUpload中的主要对象与协作关系
1.首先我们想一下,作者当时面对的问题与解决思路是什么?
多(包括大)文件上传后,数据肯定比较大,web服务器给我一个有inputStream的请求,中间是有分割标识的每个文件。那么既然有重复的内容(或某对象)反复出现,那么想到内部用到iterator来得到这么一个源对象(
循环肯定是减少重复的
基本方式),而得到源对象后,我需要的是把这个源对象保存在服务器的什么地方(应该是可配置的目标对象)。那么就是设计源对象与目标对象,另外由于是处理流,源对象的核心数据是输入流,目标对象的核心部分是输出流。难点是不能一次读入所有的输入流(上面的处理可不是apache的风格),那一次只读入一小部分数据(长度一定要大于boundary的)时,不一定正好是你要的长度,可能有多种情况,比如这部分数据可能正好读到分割线一部分,或者读到文件部分结尾还带有一部分下一段的数据。
直接看一下作者是如何解决的:核心类FileUploadBase中有这个方法用来处理请求对象。
public List<FileItem> parseRequest(RequestContext ctx)这个方法不长,而且其中主要的对象都有了。
FileItemIterator就是iterator对象,用来得到每一个分段源对象。
FileItemStreamImpl就是每一个分段源对象。
FileItemFactory就是目标对象的工厂,根据源对象的一些属性生成目标对象。
FileItem就是每一个分段的目标对象。
在每一次的iterator循环的过程中,得到源对象,再得到目标对象,再用Streams.copy把源对象中的最重要的文件流数据写入到目标对象的输出流上去。至于输出流指向哪里就看配置了。也许是
内存,也许是临时文件。
另外,MultipartStream是其中非常重要的真正处理流数据的一个对象。
2.FileItemIteratorImpl迭代器
FileItemIteratorImpl是FileUploadBase中的内部类,这个设计可以在一些容器类,比如
hashmap等源代码中看到,hashmap中耗用迭代的是keyset对象。FileItemIteratorImpl迭代的对象是FileItemStreamImpl,这个是FileUploadBase内部类的内部类了。
hasNext()与findNextItem()是FileItemIteratorImpl中的重要方法,而在构造迭代器时,有一句:multi = new MultipartStream(**),就是间接持有总的输入流。因为迭代的对象一定要从流中得到,所以迭代器持有并操作这个流。构造中先得到一些
全局性的属性,比如
编码,bundary啊之类的东西放在迭代器中存着。
3.FileItemStreamImpl迭代对象
构造好迭代器后调用findNextItem()来产生迭代对象,这时又从持有的流中得到每一个分段的信息,比如fileName,Content-Type之类的。有了这几个信息就可以new一个FileItemStreamImpl对象了,记着这个是源对象。在new源对象的过程中,除了赋几个分段简单属性外,有一句很重要:itemStream = multi.newInputStream();multi是迭代器持有的总的输入流,这时候为迭代对象赋了一个新的流对象。
3.多个输入流对象的处理
仔细看这个new出来的分段流对象itemStream的代码,
发现是总的流中的一个内部类,new这个分段流的过程中只是设置一下读取流的位置信息,而每new一次就重置一下。可以看出实际上的这么多输入流持有的最基本的输入流只有一个,就是请求中的输入流。这个基本流被迭代器的包装流multi持有并操作,而迭代过程中又被包装流multi中的小弟分段流itemStream来持有并操作,而分段流只在迭代对象生成时生成。真能装,任何对象包装了流,自己也可以叫流了。
4.目标对象中的输出流对象
List<FileItem>是处理的结果,FileItem就是处理后的每一个迭代对象对应的每一个结果对象。一旦迭代对象FileItemStreamImpl产生了,就生成FileItem对象,最核心的文件数据是怎么从迭代对象到结果对象的呢?
Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
就是上面这句,从迭代对象的那个小弟输入流中,不断的写到结果对象的输出流中。输出流是:dfos=new DeferredFileOutputStream(sizeThreshold, outputFile)对象。从参数看的出有一个size控制,上传项目文件的临时数据可以存储在内存中或
硬盘上。这个依赖于上传项目的大小(即:数据的字节)。
另外这个输出流对象还可以获取输入流,也就是整个处理完了,给用户返回List<FileItem>后,用户再从上传后的内存或者硬盘临时文件中再得到一个输入流,之后按用户的需求,可以存在文件服务器或者数据库中。dfos是上面说的输出流。
public InputStream getInputStream()
throws IOException {
if (!isInMemory()) {
return new FileInputStream(dfos.getFile());
}
if (cachedContent == null) {
cachedContent = dfos.getData();
}
return new ByteArrayInputStream(cachedContent);
}
5.其它值得学习的地方
MultipartStream持有Request输入流,给迭代对象所用的内部子流对象也是操作的Request输入流。边
readByte()//从请求流中读一部分数据放入buffer中。
findSeparator()//在buffer中找到boundary,如果只读到一部分boundary,就不读了,把buffer中的部分boundary移动到buffer前面,再读一部分数据进来。
其他还有头部的分割,回车换行的处理,细节就不分析了。都在MultipartStream中,可以学到很多处理流与buffer,以及其中字符的处理。
6.总结
FileUpload的处理不是一次性读出所有请求流数据,而是读一小块,分析一小块,如果读到分割就产生一个迭代对象,读到分割中的正文就让流去写入内存或者临时文件。读到分块结尾就再产生一个迭代对象重复上面的工作。设计上十分巧妙,不走回头路。
7.hashmap中的iterator
复习
既然提到处理重复数据时用的iterator,随便提一下hashmap,可以对比一下。
Entry<K,V>就是内部定义的迭代对象。
HashIterator是抽象的迭代器,hashmap中可以迭代Entry,也可以迭代key,还可以迭代value,核心是迭代Entry。
迭代器一般持有当前迭代值,或者下一值,还有计数等公共的东西。
hasNext()与next()一般是迭代器提供给外部的
接口方法。
抽象的迭代器与三个继承的迭代器的关系就是next不同。抽象的有一个nextEntry(),三个继承的有next()方法去调用,返回的都是Entry,只是最后取的不一样,分别是Entry,key,value而已。
内部类实现一个接口,让主类对外呈现另一功能面。而fileUpload中迭代是给自己内部使用的。