- 浏览: 99410 次
- 性别:
- 来自: 上海
文章分类
- 全部博客 (50)
- druid (1)
- java (7)
- database (1)
- 源码 (6)
- dubbo (4)
- jetty (1)
- protocol (1)
- 工作流 (3)
- 电商 (1)
- 支付平台 (1)
- 大数据 (0)
- 微信小程序 (1)
- 短信平台 (1)
- jms (1)
- jndi (1)
- spi (1)
- FileUpload (0)
- concurrent (1)
- 统计业务 (0)
- 业务 (3)
- sql (2)
- andriod (1)
- maven (1)
- OAuth (1)
- ws (1)
- spring (6)
- mybatis (1)
- java rocketmq (0)
- rocketmq (1)
- RxJava (1)
- java模式 (1)
- AI (0)
- 机器学习 (1)
- springboot (1)
- tomcat (1)
- 协议 (1)
- springcloud (1)
- stream (1)
- 技术管理 (1)
- k8s (1)
- ser (0)
- istio (1)
- ddd (1)
- 微服务 (1)
- 操作笔记 (1)
最新评论
-
herman_liu76:
luozhen89 写道讲得很好。知识后面的KAFKA跟OAu ...
尽量把OAuth2.0的原理讲透透的 -
luozhen89:
讲得很好。知识后面的KAFKA跟OAuth有什么关系,没看懂。 ...
尽量把OAuth2.0的原理讲透透的 -
herman_liu76:
ZHENFENGSHISAN 写道太累了啊,哥唉~ 我也觉得很 ...
代码快看哭了-吐槽与感悟汇总 -
ZHENFENGSHISAN:
太累了啊,哥
代码快看哭了-吐槽与感悟汇总 -
herman_liu76:
1126481146 写道厉害啊,有联系方式吗,学习学习,我现 ...
druid 源码分析与学习(含详细监控设计思路的彩蛋)
FileUpload 是 Apache commons下面的一个子项目,用来实现Java环境下面的文件上传功能,与常见的SmartUpload齐名。
上传我们一般就直接使用现成的工具来实现就好了,很多工具非常好用,如果理解了它的原理,不仅有助于选择不同的工具,还有助于处理遇到的问题,或者改进工具,或者使用工具中不知道的其它功能。
本文章分三部分:首先介绍基本上传原理,接着是重点介绍FileUpload源码中几要的几个类的协作关系,主要是内部iterator类,流的数据处理方式,以及一个输入流依次分成几股后,依次分别写入不同的输出流的过程。掌握些套路,感受下作者是设计思想。最后介绍如何监听上传进度,包括设计思想与部分代码。
一、 先说说基本的上传过程(熟悉的可以略过)
1.客户端:
客户端代码是一个form,类型enctype用multipart/form-data,这样可以把文件中的数据作为流式数据上传,不管是什么文件类型,均可上传。
浏览器会把相应的数据按请求的格式发送到web服务上的,而web服务器可以把请求转成Request对象。
请求的结构示例如下:关注【boundary】,用来对请求体分块的一个字符串。
2.服务器端:
对于服务器端的Request的处理,先举一个简单的处理过程。
二、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是上面说的输出流。
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中迭代是给自己内部使用的。
三、上传进度的监控
很多时候上传大文件,需要告诉客户端上传的进度。下面就介绍一下如何设计。
1.设计思想
通过以上的分析,我们看到真正的上传数据写入内存或者临时文件,是由目标对象FileItem的输出流来操作的,就是FileItem的实现类DiskFileItem。它里面有一个方法是getOutputStream()。那我们安排一个人来监听这个输出流的输出过程(数据大小)不就可以了。输出流做任何操作的时候,都让监听人记录下来,那这个输出流就应该被包装一下,让包装后的输出流持有源输出流,并持有监听人来记录。
我们定义这个包装后的输出流叫:MonitoredOutputStream,它持有原来的输出流和监听人。DiskFileItem里的获取输出流当然就是获取包装后的了,那这个方法要重写了,干脆DiskFileItem产生一个继承类MonitoredDiskFileItem来重写此方法。
DiskFileItemFactory是DiskFileItem的工厂类,看来也要重写了,因为它要生产的是MonitoredDiskFileItem了。我们也用继承的方式修改createItem()的方法,这样就产生了MonitoredDiskFileItemFactory类。
看来从底层发生一个变化,它的上层所有的类都要发生变化了,还好层次不深。另外我们在底层使用了一个监听人,那需要从外部传进去,最外部就是MonitoredDiskFileItemFactory了,它的构造函数里可以传监听人进去。另外监听人不能只是听,他应该有个小本子把监听到的东西记录下来。监听人自带作业本(内部类)。
2.总结一下
由于监控输出流,要有一个监听人(1个类)插入到输出流中,从此有了代理输出流(1 个类)。从而造成上层的所有类(2个类)发生变化。另外监听人自带作业本(1个内部类),监听最好弄个接口出来比较酷(1个接口)。
3.如何使用
首先是标准的上传请求过程,另外就是页面不断请求获取上传进度的数据的过程。服务器需要处理这两个请求。
3.1标准上传的过程
把监听人的作业本保存在session中。
3.2查询进度的过程
从session中得到数据,可以算出一些结果传给前台了。
4.疑问
为何我们要在FileItem的输出流上做文章呢?为何不在它的输入流上做文章呢?这是因为fileupload使用对外提供的接口很少,就几句话,如果处理那个迭代源对象FileItemStream的输入流读数据就可能就改的面目全非了,所以目标对象FileItem上的输出流上比较好处理。
另外我们看到源码中有一个notify,不知道可以通知什么?会不会是人家已经考虑到这方面的需求了?我还没研究这里,有了再补充。
四、 答疑与另一种进度监控方式
1.上面的上传监控中的问题:
又研究fileUpload中的nitify与listener,发现上面的进度条监控是存在bug的。总的上传数据量是request.getContentLength(),它是除了请求BODY的长度,包含了分割boundary,回车,还有每个文件名等属性,当然还有文件本身。
而在目标对象FileItem上的输出流来源于源对象FileItemStream的输入流,而这个输入流只是真正的文件本身。所以所有的文件正文数据加在一起是永远小于< request.getContentLength()的。当然上传的比例要不要那么精确是另一回事!所以这个也是可以用的。正文越大,精确度越高。
2.fileUpload中已经有了上传监控的考虑:
再回到fileUpload中的notify与listener,notify,看看他们的关系。
FileUploadBase中可以从外部设置进去一个setProgressListener,设置好以后,MultipartStream(持有请求流,并可生成分段流)中的静态内部类Notifier就可以new出来了。子类先有了就可以设置给父类MultipartStream了。在生成迭代器的时候有这么两句:
notifier = new MultipartStream.ProgressNotifier(listener, requestSize);//用监听器做参数生成一个通知器
multi = new MultipartStream(input, boundary, notifier);//通知器做参数生成总处理流对象。
看到上面两句有点体会:监听器都是与底层的对象输入输出打交道,而用户操作的是上层的对象,如何把监听器从外面传给底层呢?三中的方法是改写类,修改构造函数。而这里的方法用了通知与监听的两个结构来处理。通知器当成静态内部类预先埋在里面,通知器记录数据,与三中的记事本相似。可以仔细体会一下异同点。
Notifier里面持有监听器,还有几个属性:contentLength总长度,bytesRead已经读了多少,items文件个数记录。那么关系就清楚了,在总处理流的读请求流的过程中,读到数据就累加设置bytesRead,读到分段就累加设计items的数(迭代器的findNextItem方法中确实有notifier.noteItem(),用来累加个数),而在累加数据数与文件分段数的时候都调用监听器的核心方法:listener.update(bytesRead, contentLength, items);,把通知器中的值告诉监听器。
这下明白了,这个fileUpload中本身就有通知器一直记录着上传情况,你要是传个监听器就会得到上传情况。而且这个数据完全没有bug,上传的比例不仅是正文数据的比例,还包括每段正文的属性,分割字符在内的完整的body的数据的比例。
3.那我们如何从客户端得到上传情况呢?
自己设计一个监听器,可以在源码的test包中看到ProgressListenerImpl,监听器也有属性,包括总长度,读取长度,分段数,与通知器中一样。还有一个核心方法update。把监听器扔到FileUploadBase(或者ServletFileUpload)中就可以被通知了,同时把这个监听器也扔到session中去,客户端就可以反复请求获取监听器中的值了。这一点与三中的方法差不多了,就不赘述了。
欢迎看过的朋友留言、提出意见、欢迎交流~
上传我们一般就直接使用现成的工具来实现就好了,很多工具非常好用,如果理解了它的原理,不仅有助于选择不同的工具,还有助于处理遇到的问题,或者改进工具,或者使用工具中不知道的其它功能。
本文章分三部分:首先介绍基本上传原理,接着是重点介绍FileUpload源码中几要的几个类的协作关系,主要是内部iterator类,流的数据处理方式,以及一个输入流依次分成几股后,依次分别写入不同的输出流的过程。掌握些套路,感受下作者是设计思想。最后介绍如何监听上传进度,包括设计思想与部分代码。
一、 先说说基本的上传过程(熟悉的可以略过)
1.客户端:
客户端代码是一个form,类型enctype用multipart/form-data,这样可以把文件中的数据作为流式数据上传,不管是什么文件类型,均可上传。
<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中迭代是给自己内部使用的。
三、上传进度的监控
很多时候上传大文件,需要告诉客户端上传的进度。下面就介绍一下如何设计。
1.设计思想
通过以上的分析,我们看到真正的上传数据写入内存或者临时文件,是由目标对象FileItem的输出流来操作的,就是FileItem的实现类DiskFileItem。它里面有一个方法是getOutputStream()。那我们安排一个人来监听这个输出流的输出过程(数据大小)不就可以了。输出流做任何操作的时候,都让监听人记录下来,那这个输出流就应该被包装一下,让包装后的输出流持有源输出流,并持有监听人来记录。
我们定义这个包装后的输出流叫:MonitoredOutputStream,它持有原来的输出流和监听人。DiskFileItem里的获取输出流当然就是获取包装后的了,那这个方法要重写了,干脆DiskFileItem产生一个继承类MonitoredDiskFileItem来重写此方法。
DiskFileItemFactory是DiskFileItem的工厂类,看来也要重写了,因为它要生产的是MonitoredDiskFileItem了。我们也用继承的方式修改createItem()的方法,这样就产生了MonitoredDiskFileItemFactory类。
看来从底层发生一个变化,它的上层所有的类都要发生变化了,还好层次不深。另外我们在底层使用了一个监听人,那需要从外部传进去,最外部就是MonitoredDiskFileItemFactory了,它的构造函数里可以传监听人进去。另外监听人不能只是听,他应该有个小本子把监听到的东西记录下来。监听人自带作业本(内部类)。
2.总结一下
由于监控输出流,要有一个监听人(1个类)插入到输出流中,从此有了代理输出流(1 个类)。从而造成上层的所有类(2个类)发生变化。另外监听人自带作业本(1个内部类),监听最好弄个接口出来比较酷(1个接口)。
3.如何使用
首先是标准的上传请求过程,另外就是页面不断请求获取上传进度的数据的过程。服务器需要处理这两个请求。
3.1标准上传的过程
把监听人的作业本保存在session中。
//产生一个监听人,设置总的数据长度。 UploadListener listener = new UploadListener(request.getContentLength()); listener.start();// 启动监听状态,记在监听人的作业本中 // 将监听器对象的状态保存在Session中 session.setAttribute("FILE_UPLOAD_STATS", listener.getFileUploadStats()); session.setAttribute("bytesRead", "0"); // 创建MonitoredDiskFileItemFactory对象 FileItemFactory factory = new MonitoredDiskFileItemFactory(listener);//新设计的工厂 ServletFileUpload upload = new ServletFileUpload(factory);//把这个工厂给fileUpload组件使用。 List<FileItem> items = upload.parseRequest(request);//fileUpload处理请求。 listener.done();// 停止使用监听器 ....
3.2查询进度的过程
从session中得到数据,可以算出一些结果传给前台了。
UploadListener.FileUploadStats fileUploadStats = (UploadListener.FileUploadStats) session.getAttribute("FILE_UPLOAD_STATS"); if (fileUploadStats != null) { long bytesProcessed = fileUploadStats.getBytesRead();// 获得已经上传的数据大小 long sizeTotal = fileUploadStats.getTotalSize();// 获得上传文件的总大小 // 计算上传完成的百分比 long percentComplete = (long) Math.floor(((double) bytesProcessed / (double) sizeTotal) * 100.0); // 获得上传已用的时间 long timeInSeconds = fileUploadStats.getElapsedTimeInSeconds(); // 计算平均上传速率 double uploadRate = bytesProcessed / (timeInSeconds + 0.00001); // 计算总共所需时间 double estimatedRuntime = sizeTotal / (uploadRate + 0.00001);//(其它略) }
4.疑问
为何我们要在FileItem的输出流上做文章呢?为何不在它的输入流上做文章呢?这是因为fileupload使用对外提供的接口很少,就几句话,如果处理那个迭代源对象FileItemStream的输入流读数据就可能就改的面目全非了,所以目标对象FileItem上的输出流上比较好处理。
另外我们看到源码中有一个notify,不知道可以通知什么?会不会是人家已经考虑到这方面的需求了?我还没研究这里,有了再补充。
四、 答疑与另一种进度监控方式
1.上面的上传监控中的问题:
又研究fileUpload中的nitify与listener,发现上面的进度条监控是存在bug的。总的上传数据量是request.getContentLength(),它是除了请求BODY的长度,包含了分割boundary,回车,还有每个文件名等属性,当然还有文件本身。
而在目标对象FileItem上的输出流来源于源对象FileItemStream的输入流,而这个输入流只是真正的文件本身。所以所有的文件正文数据加在一起是永远小于< request.getContentLength()的。当然上传的比例要不要那么精确是另一回事!所以这个也是可以用的。正文越大,精确度越高。
2.fileUpload中已经有了上传监控的考虑:
再回到fileUpload中的notify与listener,notify,看看他们的关系。
FileUploadBase中可以从外部设置进去一个setProgressListener,设置好以后,MultipartStream(持有请求流,并可生成分段流)中的静态内部类Notifier就可以new出来了。子类先有了就可以设置给父类MultipartStream了。在生成迭代器的时候有这么两句:
notifier = new MultipartStream.ProgressNotifier(listener, requestSize);//用监听器做参数生成一个通知器
multi = new MultipartStream(input, boundary, notifier);//通知器做参数生成总处理流对象。
看到上面两句有点体会:监听器都是与底层的对象输入输出打交道,而用户操作的是上层的对象,如何把监听器从外面传给底层呢?三中的方法是改写类,修改构造函数。而这里的方法用了通知与监听的两个结构来处理。通知器当成静态内部类预先埋在里面,通知器记录数据,与三中的记事本相似。可以仔细体会一下异同点。
Notifier里面持有监听器,还有几个属性:contentLength总长度,bytesRead已经读了多少,items文件个数记录。那么关系就清楚了,在总处理流的读请求流的过程中,读到数据就累加设置bytesRead,读到分段就累加设计items的数(迭代器的findNextItem方法中确实有notifier.noteItem(),用来累加个数),而在累加数据数与文件分段数的时候都调用监听器的核心方法:listener.update(bytesRead, contentLength, items);,把通知器中的值告诉监听器。
这下明白了,这个fileUpload中本身就有通知器一直记录着上传情况,你要是传个监听器就会得到上传情况。而且这个数据完全没有bug,上传的比例不仅是正文数据的比例,还包括每段正文的属性,分割字符在内的完整的body的数据的比例。
3.那我们如何从客户端得到上传情况呢?
自己设计一个监听器,可以在源码的test包中看到ProgressListenerImpl,监听器也有属性,包括总长度,读取长度,分段数,与通知器中一样。还有一个核心方法update。把监听器扔到FileUploadBase(或者ServletFileUpload)中就可以被通知了,同时把这个监听器也扔到session中去,客户端就可以反复请求获取监听器中的值了。这一点与三中的方法差不多了,就不赘述了。
欢迎看过的朋友留言、提出意见、欢迎交流~
发表评论
-
如何设计一个结构合理的java项目
2021-02-07 20:25 344## 1、前言 最近写一个Java处理工具,是一个sprin ... -
回忆背调业务核心组件的开发
2020-01-11 00:24 620这是我第一次做jav ... -
spring与tomcat的关系逆袭前后的源码设计分析
2019-12-25 00:15 713补: 最近回看了 ... -
一张图说明netty的结构及源码分析后的总结
2019-03-22 17:39 1131好久没写技术文章了,根据往深层次学习的大方向,学习了n ... -
maven初探
2017-05-18 10:46 589以前和一个做互联 ... -
log4j使用、源码简析与怎么出炉呢
2017-04-08 19:34 466本文根据log4j-1.2.17,先介绍开发J2EE中 ... -
java并发知识汇总
2017-02-23 16:57 674项目中用到并发的 ... -
从JMS标准看接口编程、模块整合及相关技术jndi,spi等
2017-02-12 22:50 974最近看了一篇微信 ... -
中国移动企业短信通平台EMPP协议分析
2017-02-08 20:02 825中国移动企信通地址:http://sms.sh.chinamo ... -
dubbo注册部分源码分析、集群策略、负载均衡算法
2016-07-29 21:41 3456前面分别写了二篇文章,介绍dubbo的源码与模拟现场场 ... -
druid 源码分析与学习(含详细监控设计思路的彩蛋)
2016-07-01 13:48 17906Druid是阿里巴巴公司的数据库连接池工具,昨天突然想 ...
相关推荐
7. **Apache Commons FileUpload** (commons-fileupload-1.2.jar): 这是专门为处理HTTP请求中的文件上传设计的库,提供了解析多部分请求、处理文件上传的核心功能。这是实现文件上传功能的关键依赖。 8. **Apache ...
在ASP.NET中,FileUpload控件是用于处理用户上传文件的核心组件。然而,标准的FileUpload控件并不直接支持显示上传进度条,因为这需要浏览器和服务器之间进行实时通信来更新进度状态。为了实现“完美进度条”,我们...
这里我们探讨的主题是“spring文件上传代码”,这涉及到Spring MVC如何处理文件上传请求,以及如何实现通用的Excel导入功能。我们将从Java微服务的视角出发,讨论相关依赖包和关键组件。 1. **文件上传**: - ...
在IT行业中,文件上传与下载是Web应用中的基础功能,广泛应用于各种系统,如云存储、社交媒体、在线教育等。本资源"文件上传与下载源代码"提供了在MyEclipse环境下实现这一功能的具体示例。MyEclipse是一款强大的...
- 其次,利用JavaScript处理文件选择和验证,如检查文件类型、大小等。 - 接着,通过Ajax异步上传文件,服务器端接收文件并存储到指定位置。 - 最后,更新数据库或者其他数据结构来记录文件信息,以便后续的访问...
7. **性能优化**:为了提高性能,FileUpload允许设置上传进度监听器,可以实时监控上传进度。此外,可以配置异步处理文件上传,以减少主线程阻塞。 8. **错误处理**:在处理上传过程中可能会遇到各种异常,如文件过...
服务器端主要负责处理文件上传逻辑和状态的监控。 2.1.1. 文件上传状态类(FileUploadStatus) `FileUploadStatus`类用于存储和更新文件上传的状态,如上传进度、文件地址等。它包含如上传地址、已读取大小、文件...
标题"struts2+dwr+自己实现的progresslistener监控上传进度"表明我们将讨论如何在Struts2和DWR的结合中,通过自定义的ProgressListener来实现文件上传进度的实时监控。这在大型文件上传时尤其重要,因为它提供了用户...
JSP可以包含Java代码片段,用于调用后台Servlet处理文件上传逻辑。 8. **错误处理和日志记录**: 确保程序包含适当的错误处理,如文件上传失败或下载出错时向用户显示友好的错误信息。日志记录可以帮助调试和监控...
在Java Web开发中,文件上传和下载是常见的...总的来说,Java Web中的文件上传涉及前端表单设计、后端Servlet处理和第三方库的使用。通过这些技术,我们可以构建出稳定、可靠的文件管理系统,满足各种Web应用的需求。
处理文件上传时,还需要考虑错误处理,如文件大小超出限制、文件类型不合法等。 总结一下,Struts03的拦截器和过滤器都是提升应用程序效率和功能的重要工具,但它们的作用范围和应用场景各有侧重。理解并熟练运用这...
Commons FileUpload处理文件上传,实现与用户的交互;而JSTL则简化了JSP页面的开发,使代码更易于维护。HTTPClient则在后端与外部服务通信时起到关键作用。这些库共同构成了一个坚实的基础,支持开发人员构建高效、...
7. **commons-io-2.2.jar**:Apache Commons IO库提供了大量的IO操作辅助类,如文件读写、流处理等,对于处理文件上传等任务非常实用。 8. **commons-fileupload-1.3.2.jar**:Apache Commons FileUpload是处理HTTP...
图片上传程序是一个常见的功能,广泛应用于各种Web应用和社交媒体平台,允许用户方便...分析和学习这些代码可以帮助我们深入了解如何用Java实现一个完整的图片上传系统,包括其架构设计、代码组织以及具体的实现细节。
文件上传完成后,服务器端需要处理文件,如保存到指定目录、验证文件类型和大小等。 2. 文件下载功能: 文件下载则涉及将服务器上的文件发送到客户端。ASP.NET可以通过HttpResponse对象的WriteFile方法来实现。在...
在Struts中,可以使用Struts2的FileUpload插件来处理文件上传。该插件支持多文件上传,且能处理大文件,防止内存溢出。你需要配置struts.xml文件,声明可以接收文件上传的Action,并指定允许的文件类型和大小。 在...
为了实时监控上传进度,需要编写监听器类来捕获上传过程中的事件。这些监听器将与Struts框架配合工作,确保上传状态能够及时反馈给前端,进而更新进度条。 ### 结论 通过结合DWR和Struts,可以实现一个功能完善且...
5. 文件上传相关的库,如commons-fileupload和commons-io,提供文件上传功能的支持。 6. 其他辅助库,如slf4j和logback,用于日志记录。 在项目的lib目录下,这些jar包应该已经被正确放置,以确保项目能够正常运行...
本文将深入探讨Java中处理文件上传的关键知识点,包括jsp源码的使用、文件服务的实现以及相关的最佳实践。 1. **Servlet与JSP**: - **Servlet**:Java Servlet是一种Java类,用于扩展服务器的功能,通常用于处理...