struts2 获取文件上传进度方法

一、上传机制

1. struts2 的 Dispacher 类中 wrapRequest 方法, 将http请求头中带有multipart/form-data 开头的请求都会包装成为 MultiPartRequest 的实例

 Dispacher.wrapRequest

这里 MultiPartRequestWrapper 构造方法就是用MultiPartRequest 再包装一层, 同时还会调用他的parse 方法, 进行文件的上传

2. 这里getContainer().getInstance 得到的对象, 是从struts的 ObjectFactory中得到的(可以在对应的struts配置文件的<bean>标签找到), 查看struts默认配置文件 struts-default.xml 可以看到下面这一句

<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" 
name="jakarta" class="org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest" scope="default" optional="true" />

这里的定义, 可以理解为:  Interface obj = cls.newInstance() 写法变成xml而已

default里面定义了两个, name 分别是 struts 和 jakarata, 作用却是完全一样的, 支持文件的分块上传, 这样大文件的上传才不会出问题.

3. 进入JakartaMultiPartRequest 类, 查看 parse方法代码

可以看出, 这里用的是 commons-fileupload 包来做文件上传的.

4. commons-fileupload 包中, ServletFileUpload 类可以使用一个 progressListener 回调对象来监听上传进度, 而这个JakartaMultiPartRequest 类中已经写死了, 没有使用任何listener:

 ServletFileUpload upload = new ServletFileUpload(fac);
 upload.setSizeMax(maxSize);
 List items = upload.parseRequest(createRequestContext(servletRequest));

这时候因为没有注入到监听器, 而且在这里会parseRequest, 也就是把文件上传过来并保存在saveDir目录中, 最后走到action,交给业务去处理。

以上是整个上传的机制,struts2通过封装commons-fileupload,直接免去了文件上传代码的编写,需要做的仅仅是在action里面保存文件以及一些后续工作而已。

但是上面的第4点中提到,因为代码写死了,没有指定监听器,也不能通过注入的形式(因为ServletFileUpload对象是临时new出来的),所以导致文件上传的过程中我们无法知道进度。

二、解决方案

5. 先理解为什么struts2为什么会使用jakarta来包装文件上传的请求,其实是在

     struts.multipart.parser 这个常量中指定的,默认情况下

   <constant name="struts.multipart.parser" value="jakarta"></constant>

   这里的value,就是在struts-default.xml 中定义的bean的name  (见第2节)

6. 所以解决的方法是, 我们自己来实现MultiPartRequest  这个接口, 然后自己定义一个bean, 比如叫 jakartaExt, 然后把5中的parser常量的值设置为jakartaExt, 就可以替换系统默认的包装, 用我们自己定义的了, 这个时候我们就可以给ServletFileUpload对象注入listener了.

注入listener
配置

注: 这里的uploadId仅是为了区分不同的上传线程而已(同一个用户可以开多个页面同时上传)

尝试上传文件, 可以看到日志打出来的结果:

2013-03-06 11:48:15,DEBUG,file upload, url=http://localhost/struts2/upload.htm
2013-03-06 11:48:15,DEBUG,[1362541661052] upload progress:0%
2013-03-06 11:48:28,DEBUG,[1362541661052] upload progress:10%
2013-03-06 11:48:38,DEBUG,[1362541661052] upload progress:20%
2013-03-06 11:48:48,DEBUG,[1362541661052] upload progress:30%
2013-03-06 11:48:58,DEBUG,[1362541661052] upload progress:40%
2013-03-06 11:49:08,DEBUG,[1362541661052] upload progress:50%
2013-03-06 11:49:19,DEBUG,[1362541661052] upload progress:60%
2013-03-06 11:49:30,DEBUG,[1362541661052] upload progress:70%
2013-03-06 11:49:40,DEBUG,[1362541661052] upload progress:80%
2013-03-06 11:49:52,DEBUG,[1362541661052] upload progress:90%
2013-03-06 11:50:05,DEBUG,[1362541661052] upload progress:100%
2013-03-06 11:50:05,INFO,[1362541661052] contentLength:2869568 parse use 109772ms

这样, 我们就可以通过ajax的形式, 从session中获取到对应uploadId的上传进度, 页面也就可以做出进度条了.

7. 另一种解决方案

     就是采用servlet来上传, 自己也用commons-fileupload组件来操作, 要注意的是, 把web.xml中struts2的filter-mapping 不能是 /*, 而是应该对应的 /*.action /*.htm 之类的, 否则如果拦截了所有请求, 这时候会先经过struts2的 filter而导致request被wrap掉(wrap的过程就会有parse的操作),导致servlet有响应的时候, 实际上已经上传好了...

我个人还是倾向6的做法, 因为这时候技能做到上传与业务无关, 也能监控进度, 如果在servlet中做的话, 你每上传一种文件都要一段新的处理逻辑挤进去(因为每种文件的处理逻辑不一样)