文件上传的整个流程:
第一阶段: 构造struts2中针对请求字节流而构造的封闭类MultiPartRequestWrapper
1.FilterDispatcher
在doFilter方法中调用了prepareDispatcherAndWrapRequest方法,为了包装出Struts2自己的request对 象,在prepareDispatcherAndWrapRequest方法中调用Dispatcher类的wrapRequest方法,在这个方法里, 会根据请求内容的类型(提交的是文本的,还是multipart/form-data格式),决定是使用tomcat的 HttpServletRequestWrapper类分离出请求中的数据,还是使用Struts2的MultiPartRequestWrapper来 分离请求中的数据。
注:向服务器请求时,数据是以流的形式向服务器提交,内容是一些有规则东东,我们平时在jsp中用request内置对象取parameter时,实际上是由tomcat的HttpServletRequestWrapper类分解好了的,无需我们再分解这些东西了。
当然,在这里,我们研究的是上传文件的情况,所以,由于form中设定的提交内容是媒体格式的,所以,Dispatcher类的wrapRequest方法会将请求交由MultiPartRequestWrapper类来处理。
MultiPartRequestWrapper这个类是Struts2的类,并且继承了tomcat的 HttpServletRequestWrapper类,也是我们将用来代替HttpServletRequest这个类的类,看名字也知道,是对多媒体 请求的包装类。
Struts2本身当然不会再造个轮子,来解析请求,而是交由Apache的commons-fileupload组件来解析了。
在MultiPartRequestWrapper的构造方法中,会调用MultiPartRequest(默认为JakartaMultiPartRequest类)的parse方法来解析请求。
注:MultiPartRequestWrapper最终的一些操作比如getParameter都是通过调用JakartaMultiPartRequest相应的方法完成的.所以,MultiPartRequestWrapper,JakartaMultiPartRequest都看成是request. 只是有个委托的关系.
在Struts2的JakartaMultiPartRequest类的parse方法中才会真正来调用commons-fileupload组件的 ServletFileUpload类对请求进行解析,至此,Struts2已经实现了将请求转交commons-fileupload组件对请求解析的 全过程。
剩下的就是等commons-fileupload组件对请求解析完毕后,拿到分解后的数据,根据field名,依次将分解后的field名和值放到 params(HashMap类型)里,同时JakartaMultiPartRequest类重置了HttpServletRequest的好多方法, 比如熟知的getParameter、getParameterNames、getParameterValues,实际上都是从解析后得到的那个 params对象里拿数据,在这个过程,commons-fileupload组件也乖乖的把上传的文件分析好 了,JakartaMultiPartRequest也毫不客气的把分解后的文件一个一个的放到了files(HashMap类型)中,实际上此 时,commons-fileupload组件已经所有要上传的文件上传完了。
至此,Struts2实现了对HttpServletRequest类的包装,当回到MultiPartRequestWrapper类后,再取一下上述解析过程中发生的错误,然后把错误加到了自己的errors列表中了。
同样我们会发现在MultiPartRequestWrapper类中,也把HttpServletRequest类的好多方法重载了,毕竟是个包装类 嘛,实际上对于上传文件的请求,在Struts2后期的处理中用到的request都是MultiPartRequestWrapper类对象,比如我们 调用getParameter时,直接调用的是MultiPartRequestWrapper的getParameter方法,间接调的是 JakartaMultiPartRequest类对象的getParameter方法。
第二阶段: 执行inteceptor及action.
doFilter方法中,会进一步调用actionMapper的getMapping方法对url进行解析,找出命名空间和action名等,以备后面根据配置文件调用相应的拦截器和action使用。
关于doFilter方法中下一步对Dispatcher类的serviceAction方法的调用,不再描述,总之在action被调用之前,会首先走 到fileUpload拦截器(对应的是FileUploadInterceptor类),在这个拦截器中,会先看一下request是不是 MultiPartRequestWrapper,如果不是,就说明不是上传文件用的request,fildUpload拦截器会直接将控制权交给下一 个拦截器;如果是,就会把request对象强转为MultiPartRequestWrapper对象,然后调用hasErrors方法,看看有没有上 传时候产生的错误,有的话,就直接加到了Action的错误(Action级别的)中了。
另外,在fileUpload拦截器中会将MultiPartRequestWrapper对象中放置的文件全取出来,把文件、文件名、文件类型取出来, 放到request的parameters中,这样到了params拦截器时,就可以轻松的将这些内容注入到Action中了,这也就是为什么 fileUpload拦截器需要放在params拦截器前面的理由。在文件都放到request的parameters对象里之后,fileUpload 拦截器会继续调用其他拦截器直到Action等执行完毕,他还要做一个扫尾的工作:把临时文件夹中的文件删除(这些文件是由commons- fileupload组件上传的,供你在自己的Action中将文件copy到指定的目录下,当action执行完了后,这些临时文件当然就没用了)。
详细代码跟踪:
Dispatcher:
public HttpServletRequest wrapRequest(HttpServletRequest request, ServletContext servletContext) throws IOException {
// don't wrap more than once
if (request instanceof StrutsRequestWrapper) {
return request;
}
String content_type = request.getContentType();
//根据表单的提交类型转换成MultiPartRequestWrapper.
if (content_type != null && content_type.indexOf("multipart/form-data") != -1) {
MultiPartRequest multi = getContainer().getInstance(MultiPartRequest.class);
request = new MultiPartRequestWrapper(multi, request, getSaveDir(servletContext));
} else {
request = new StrutsRequestWrapper(request);
}
return request;
}
->
//建立 MultiPartRequestWrapper 时解析(parse) request:
public MultiPartRequestWrapper(MultiPartRequest multiPartRequest, HttpServletRequest request, String saveDir) {
super(request);
//JakartaMultiPartRequest
multi = multiPartRequest;
try {
//parse的过程中,会判断是否超过struts.multipart.maxSize,感觉stack里面的parameter={}是在parse有错误的时候清空的.
multi.parse(request, saveDir);
//如果上传的文件超过struts.multipart.maxSize,会在这里加一个错误.
for (Object o : multi.getErrors()) {
//错误信息:the request was rejected because its size (12555091) exceeds the configured maximum (5242880)
String error = (String) o;
addError(error);
}
} catch (IOException e) {
addError("Cannot parse request: "+e.toString());
}
}
附:
//JakartaMultiPartRequest实现自MultiPartRequest接口. 如果想替换JakartaMultiPartRequest,我们要做的也是实现该接口.
在JakartaMultiPartRequest的parse方法里有下面的代码片段:
public void parse(HttpServletRequest servletRequest, String saveDir)
throws IOException {
DiskFileItemFactory fac = new DiskFileItemFactory();
// Make sure that the data is written to file
fac.setSizeThreshold(0);
if (saveDir != null) {
fac.setRepository(new File(saveDir));
}
// Parse the request
try {
//这里是parse的核心代码.将parse的又委托给common-fileupload里的ServletFileUpload,ServletFileUpload extends
//FileUpload,FileUpload extends FileUploadBase, 最终是调用了FileUploadBase的parseRequest
ServletFileUpload upload = new ServletFileUpload(fac);
upload.setSizeMax(maxSize);
List items = upload.parseRequest(createRequestContext(servletRequest));
//----
for (Object item1 : items) {
FileItem item = (FileItem) item1;
if (log.isDebugEnabled()) log.debug("Found item " + item.getFieldName());
if (item.isFormField()) {
log.debug("Item is a normal form field");
List<String> values;
if (params.get(item.getFieldName()) != null) {
values = params.get(item.getFieldName());
} else {
values = new ArrayList<String>();
}
// note: see http://jira.opensymphony.com/browse/WW-633
// basically, in some cases the charset may be null, so
// we're just going to try to "other" method (no idea if this
// will work)
String charset = servletRequest.getCharacterEncoding();
if (charset != null) {
values.add(item.getString(charset));
} else {
values.add(item.getString());
}
params.put(item.getFieldName(), values);
} else {
log.debug("Item is a file upload");
// Skip file uploads that don't have a file name - meaning that no file was selected.
if (item.getName() == null || item.getName().trim().length() < 1) {
log.debug("No file has been uploaded for the field: " + item.getFieldName());
continue;
}
List<FileItem> values;
if (files.get(item.getFieldName()) != null) {
values = files.get(item.getFieldName());
} else {
values = new ArrayList<FileItem>();
}
values.add(item);
files.put(item.getFieldName(), values);
}
}
} catch (FileUploadException e) {
log.error(e);
errors.add(e.getMessage());
}
}
->
下面是common-fileupload 1.2版本中的FileUploadBase里的方法:
public List /* FileItem */ parseRequest(RequestContext ctx)
throws FileUploadException {
try {
//与之前版本相比, 将判断文件大小等重构到getItemIterator.
FileItemIterator iter = getItemIterator(ctx);
List items = new ArrayList();
FileItemFactory fac = getFileItemFactory();
if (fac == null) {
throw new NullPointerException(
"No FileItemFactory has been set.");
}
while (iter.hasNext()) {
FileItemStream item = iter.next();
FileItem fileItem = fac.createItem(item.getFieldName(),
item.getContentType(), item.isFormField(),
item.getName());
try {
Streams.copy(item.openStream(), fileItem.getOutputStream(),
true);
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new IOFileUploadException(
"Processing of " + MULTIPART_FORM_DATA
+ " request failed. " + e.getMessage(), e);
}
items.add(fileItem);
}
return items;
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new FileUploadException(e.getMessage(), e);
}
}
->
getItemIterator(ctx)方法代码:
public FileItemIterator getItemIterator(RequestContext ctx)
throws FileUploadException, IOException {
return new FileItemIteratorImpl(ctx);
}
->
FileItemIteratorImpl(RequestContext ctx)
throws FileUploadException, IOException {
if (ctx == null) {
throw new NullPointerException("ctx parameter");
}
String contentType = ctx.getContentType();
if ((null == contentType)
|| (!contentType.toLowerCase().startsWith(MULTIPART))) {
throw new InvalidContentTypeException(
"the request doesn't contain a "
+ MULTIPART_FORM_DATA
+ " or "
+ MULTIPART_MIXED
+ " stream, content type header is "
+ contentType);
}
InputStream input = ctx.getInputStream();
if (sizeMax >= 0) {
int requestSize = ctx.getContentLength();
if (requestSize == -1) {
input = new LimitedInputStream(input, sizeMax) {
protected void raiseError(long pSizeMax, long pCount)
throws IOException {
FileUploadException ex =
new SizeLimitExceededException(
"the request was rejected because"
+ " its size (" + pCount
+ ") exceeds the configured maximum"
+ " (" + pSizeMax + ")",
pCount, pSizeMax);
//我们打印的运行时异常就来自这里.
throw new FileUploadIOException(ex);
}
};
} else {
if (sizeMax >= 0 && requestSize > sizeMax) {
throw new SizeLimitExceededException(
"the request was rejected because its size ("
+ requestSize
+ ") exceeds the configured maximum ("
+ sizeMax + ")",
requestSize, sizeMax);
}
}
}
String charEncoding = headerEncoding;
if (charEncoding == null) {
charEncoding = ctx.getCharacterEncoding();
}
boundary = getBoundary(contentType);
if (boundary == null) {
throw new FileUploadException(
"the request was rejected because "
+ "no multipart boundary was found");
}
notifier = new MultipartStream.ProgressNotifier(listener,
ctx.getContentLength());
multi = new MultipartStream(input, boundary, notifier);
multi.setHeaderEncoding(charEncoding);
skipPreamble = true;
findNextItem();
}
->
在wrapRequest方法执行完之后,才会进入拦截器FileUploadInteceptor的intercept方法
在该方法中会将MultiPartRequestWrapper的error转换为action的error.
......
if (multiWrapper.hasErrors()) {
for (Iterator errorIter = multiWrapper.getErrors().iterator(); errorIter.hasNext();) {
String error = (String) errorIter.next();
if (validation != null) {
validation.addActionError(error);
}
log.error(error);
}
}
......
-------------------------------------------------------------------------------------------
struts2里面struts.properties可以显示的配置自定义的MultiPartRequest实现类.而不是使用struts2默认的
org.apache.struts2.dispatcher.multipart.JakartaMultiPartRequest,这种方式common-fileupload提示信息不能国际化.
如果修改代码,只记日志,不抛出运行时异常来避免页面提交参数丢失.但是引出的问题是如果不抛出运行时异常,是不能阻止文件的上传的. 所以,有人认为还是要抛出运行时异常,同时将错误返回到另一个页面.利用浏览器的回退来复原最初表单的提交数据.
下面是struts2提供的重置struts.multipart.parser的方式:
<bean type="org.apache.struts2.dispatcher.multipart.MultiPartRequest" name="jakarta_yourself"
class="com.xxxxx.util.JakartaMultiPartRequest"
cope="default" optional="true" />
struts.multipart.parser=jakarta_yourself
另一种解决方案是抛弃common-fileupload,其它可选的开源上传方式是smart, cos.
推荐使用cos进行文件的上传.具体可以参考http://www.iteye.com/topic/316626