问题描述:
--struts2中文件上传的二个限制,
第一道是 struts.multipart.maxSize,如果不设置,struts2 的核心包下的default.properties文件里有默认的大小设置struts.multipart.maxSize=2097152,即2M. 这是struts2文件上传的第一道关.
第二道关是 inteceptor中的maximumSize. 当真实的文件大小能通过第一道关时.针对不同的action中配置的inteceptor,maximumSize才能发挥相应的拦截作用.
比如struts.multipart.maxSize=50M. actionA中inteceptorA的maximumSize=30M. actionB中inteceptorB的maximumSize=10M.
struts.multipart.maxSize=50M对于inteceptorA,B都会起到第一关的作用.而inteceptorA和inteceptorB可以在通过第一关之后,根据自己的业务定制各自针对拦截器起作用的maximumSize
1.1.先判断表单提交的数据大小,是否超过配置文件“struts.multipart.maxSize”的值
通过struts.multipart.maxSize属性来对文件大小进行限定时,将直接影响到commons-fileupload组件的文件大小设定,默认是2M。当上传文件超过了这个尺寸时,将从commons-fileupload组件中抛出SizeLimitExceededException异常。Struts2上传文件拦截器捕获到这个异常后,将直接把该异常信息设置为Action级别的错误信息。
如果真实的文件>50M. 抛出会抛出the request was rejected because its size (XXXX) exceeds the configured maximum (XXXX)异常,他是不能被国际化的,因为这个信息是commons-fileupload组件抛出的,是不支持国际化这信息.
2.再判断上传的文件数据是否超过拦截器里配置的“maximumSize”的大小;
fileUpload拦截器只是当文件上传到服务器上之后,才进行的文件类型和大小判断。Struts2框架底层默认用的是apache的commons-fileupload组件对上传文件进行接受处理。
如果此处不满足,则会报出下面信息:
警告: File too large: upload "DSC00034.JPG" "upload_38f8bfe7_12a07e9fd3d__7ff2_00000001.tmp" 4652362
2013-5-25 20:23:59 com.opensymphony.xwork2.util.logging.commons.CommonsLogger info
信息: Removing file upload D:/Java/Tomcat6/work/Catalina/localhost/Struts2BaseDemo/upload_38f8bfe7_12a07e9fd3d__7ff2_00000001.tmp
3.最后检查是否文件类型是否满足拦截器的条件
如果此处不满足,则会报出下面信息:
警告: 您上传的文件不是图片类型!
2013-5-25 18:19:54 com.opensymphony.xwork2.util.logging.commons.CommonsLogger info
信息: Removing file upload D:/Java/Tomcat6/work/Catalina/localhost/Struts2BaseDemo/upload_38f8bfe7_12a07e9fd3d__7ff3_00000000.tmp
struts2.2 org.apache.commons.fileupload.FileUploadBase.java中
/**
* Creates a new instance.
* @param ctx The request context.
* @throws FileUploadException An error occurred while
* parsing the request.
* @throws IOException An I/O error occurred.
*/
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();
如果InteceptorA上传的是40M的真实文件.那么此时拦截器InteceptorA会访问国际化信息:struts.messages.error.file.too.larges对应的值.
当且仅当上传文件<=30M的时候,InteceptorA才会成功上传.
下面是解决struts.multipart.maxSize提示信息不友好的问题.
当超过50M时.commons-fileupload抛出运行时异常,struts2会把这个异常看到是action级别的异常.所以会将异常信息
the request was rejected because its size (XXXX) exceeds the configured maximum (XXXX)写到actionError里面.我们需要做的就是在action里覆盖addActionError方法
/**
*
* 替换文件上传中出现的错误信息
* 引用 import java.util.regex.Matcher; import
* java.util.regex.Pattern;
*
* */
@Override
public void addActionError(String anErrorMessage) {
// 这里要先判断一下,是我们要替换的错误,才处理
if (anErrorMessage
.startsWith("the request was rejected because its size")) {
Matcher m = Pattern.compile("\\d+").matcher(anErrorMessage);
String s1 = "";
if (m.find())
s1 = m.group();
String s2 = "";
if (m.find())
s2 = m.group();
// 将信息替换掉
super.addActionError("你上传的文件大小(" + s1 + ")超过允许的大小(" + s2 + ")");
// 也可以改为在Field级别的错误
// super.addFieldError("file","你上传的文件大小(" + s1 + ")超过允许的大小(" + s2 +
// ")");
} else {// 否则按原来的方法处理
super.addActionError(anErrorMessage);
}
}
addActionError()级别的错误可以用<s:actionerror/>标签在页面上显示错误消息
addFieldError() 级别错误可以用<s:fielderror/> 标签在页面上显示错误消息
注意,由于inteceptor中途返回,原来页面上输入的其他文本内容也都不见了,也就是说params注入失败。
这个是没办法的,因为这个异常是在文件上传之前捕获的,文件未上传,同时params也为注入,所以这时最好重定向到一个jsp文件,提示上传失败,然后重写填写相应信息。
解决办法:最好跳到一个专门显示错误的页.而不要返回操作页.
-------------------------------------------------------------------------------------------
注意,拦截器所谓的同名配置覆盖,是重复执行的,比如defaultStack中是包含fileUpload,token的. 如果将<interceptor-ref name="defaultStack" />放到显示定义的拦截器之后,会覆盖显示定义的拦截器.
下面是正确的拦截器顺序:
<action name="BatchMIADOperation!*" method="{1}"
class="com.*****.***.action.multiidea.batchad.BatchMIADOperationAction">
<interceptor-ref name="defaultStack" />
<interceptor-ref name="fileUpload">
<param name="maximumSize">5242880</param>
<!--
<param name="allowedTypes">
application/vnd.ms-excel
</param>
-->
</interceptor-ref>
<interceptor-ref name="token">
<param name="excludeMethods">
init,search,updateBatchCpcMatch,batchExportMIAD,downloadWhenError
</param>
</interceptor-ref>
<result name="input">
/WEB-INF/jsp/multiidea/batchad/BatchMIAD.jsp
</result>
<result name="success">
/WEB-INF/jsp/multiidea/batchad/BatchMIAD.jsp
</result>
<result name="invalid.token">
/WEB-INF/jsp/multiidea/batchad/BatchMIAD.jsp
</result>
</action>
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
经过我的测试和对源代码的Debug,如果上传文件大于2M时,在页面上就出现了一堆英文的错误信息,大致是:therequestwas rejected because its size....exceeds theconfiguredmaximum...并且在fieUpload中将来自MultiPartRequestWrapper型request对象的错误信息给加到了Action的错误中。
这时候,你在ApplicationResources.properties中自定义的上传文件过大的错误信息根本不起作用。原因就如书上所言,在底层commons-fileupload组件中就把异常给抛出来了文件根本没被上传,所以到了fileUpload拦截器时,根据取不到文件,当然也就没法对文件的类型和大小进行判断了。
然而,这个异常直接带来两个问题:
1、在页面上显示了英文的错误信息。这样的信息显然不是我们想要的。
2、由于错误的产生,原来页面上输入的其他文本内容也都不见了,也就是说params注入失败。
带着这两个问题,我们来探寻一下Struts2对于请求的处理过程。
注:这并不是一篇关于Struts2请求过程的介绍,主要是为了解决以上两个问题,才引起的简单分析。
首先当然我们要拿FilterDispatcher开刀。
在doFilter方法中调用了prepareDispatcherAndWrapRequest方法,为了包装出Struts2自己的request对象,在prepareDispatcherAndWrapRequest方法中调用Dispatcher类的wrapRequest方法,在这个方法里,会根据请求内容的类型(提交的是文本的,还是multipart/form-data格式),决定是使用tomcat的HttpServletRequestWrapper类分离出请求中的数据,还是使用Struts2的MultiPartRequestWrapper来分离请求中的数据。
注:向服务器请求时,数据是以流的形式向服务器提交,内容是一些有规则东东,我们平时在jsp中用request内置对象取meter时,实际上是由tomcat的HttpServletRequestWrapper类分解好了的,无需我们再分解这些东西了。
当然,在这里,我们研究的是上传文件的情况,所以,由于form中设定的提交内容是媒体格式的,所以,Dispatcher类的wrapRequest方法会将请求交由MultiPartRequestWrapper类来处理。
MultiPartRequestWrapper这个类是Struts2的类,并且继承了tomcat的HttpServletRequestWrapper类,也是我们将用来代替HttpServletRequest这个类的类,看名字也知道,是对多媒体请求的包装类。
Struts2本身当然不会再造个轮子,来解析请求,而是交由Apache的commons-fileupload组件来解析了。
在MultiPartRequestWrapper的构造方法中,会调用MultiPartRequest(默认为JakartaMultiPartRequest类)的parse方法来解析请求。
在Struts2的JakartaMultiPartRequest类的parse方法中才会真正来调用commons-fileupload组件的ServletFileUpload类对请求进行解析,至此,Struts2已经实现了将请求转交commons-fileupload组件对请求解析的全过程。剩下的就是等commons-fileupload组件对请求解析完毕后,拿到分解后的数据,根据field名,依次将分解后的field名和值放到ms(HashMap类型)里,同时JakartaMultiPartRequest类重置了HttpServletRequest的好多方法,比如熟知的getmeter、getmeterNames、getmeterValues,实际上都是从解析后得到的那个ms对象里拿数据,在这个过程,commons-fileupload组件也乖乖的把上传的文件分析好了,JakartaMultiPartRequest也毫不客气的把分解后的文件一个一个的放到了files(HashMap类型)中,实际上此时,commons-fileupload组件已经所有要上传的文件上传完了。
至此,Struts2实现了对HttpServletRequest类的包装,当回到MultiPartRequestWrapper类后,再取一下上述解析过程中发生的错误,然后把错误加到了自己的errors列表中了。同样我们会发现在MultiPartRequestWrapper类中,也把HttpServletRequest类的好多方法重载了,毕竟是个包装类嘛,实际上对于上传文件的请求,在Struts2后期的处理中用到的request都是MultiPartRequestWrapper类对象,比如我们调用getmeter时,直接调用的是MultiPartRequestWrapper的getmeter方法,间接调的是JakartaMultiPartRequest类对象的getmeter方法。
注:从这里,我们就可以看出,JakartaMultiPartRequest是完全设计成可以替换的类了。
然后继续向回返,到了Dispatcher类的wrapRequest方法,直接把MultiPartRequestWrapper对象返回了,我们就终于回到了FilterDispatcher类的prepareDispatcherAndWrapRequest方法,此时,我们拿到了完全解析好了的request对象(MultiPartRequestWrapper类),该对象又进一步被返回到了FilterDispatcher类的doFilter方法,也就是回到了出发点,至此,doFilter中拿到的request对象就是一个将请求中的数据分解好的了HttpServletRequest对象,我们完全可以用getmeter方法取其中的数据了,同时,我们也可以用getFiles得到文件数组了。
doFilter方法中,会进一步调用actionMapper的getMapping方法对url进行解析,找出命名空间和action名等,以备后面根据配置文件调用相应的拦截器和action使用。
关于doFilter方法中下一步对Dispatcher类的serviceAction方法的调用,不再描述,总之在action被调用之前,会首先走到fileUpload拦截器(对应的是FileUploadInterceptor类),在这个拦截器中,会先看一下request是不是MultiPartRequestWrapper,如果不是,就说明不是上传文件用的request,fildUpload拦截器会直接将控制权交给下一个拦截器;如果是,就会把request对象强转为MultiPartRequestWrapper对象,然后调用hasErrors方法,看看有没有上传时候产生的错误,有的话,就直接加到了Action的错误(Action级别的)中了。
另外,在fileUpload拦截器中会将MultiPartRequestWrapper对象中放置的文件全取出来,把文件、文件名、文件类型取出来,放到request的meters中,这样到了ms拦截器时,就可以轻松的将这些内容注入到Action中了,这也就是为什么fileUpload拦截器需要放在ms拦截器前面的理由。在文件都放到request的meters对象里之后,fileUpload拦截器会继续调用其他拦截器直到Action等执行完毕,他还要做一个扫尾的工作:把临时文件夹中的文件删除(这些文件是由commons-fileupload组件上传的,供你在自己的Action中将文件copy到指定的目录下,当action执行完了后,这些临时文件当然就没用了)。
你好,你还在看吗?呵呵,是不是太多了,也太乱了,没办法,Struts2就是这样的调用的。也不知道Struts2有没有公开其Sequence图,我是想画一个,不过,太懒,还是看着代码说说吧。
如果上面看烦了,也完全可以不看了,直接看下面的。
在上面一番分析之后,文件上传的全过程就结束了。
我们回到我们的问题上来。
先看第一个:
1、在页面上显示了英文的错误信息。这显然不是我们想要的。
没办法了,commons-fileupload组件没想到国际化,在FileUploadInterceptor拦载器中,也没想着国际化,直接放到Action的错误中了,就没他事了,三种做法:
(1)在错误显示之前,把这条错误给换掉,应该难度不大,我没做留给你做了。
(2)或者重写一下JakartaMultiPartRequest这个类,把捕捉到的异常信息换成自己的,然后,通过Struts2的配置文件,把我们重写的这个parser换上去用。
(3)直接改commons-fileupload组件的类,换成中文的。
我具体说一下第(3)种做法:找到FileUploadBase类,把902行~908行改一下。
FileUploadException ex =
new SizeLimitExceededException(
"the request was rejected because"
+ " its size (" + pCount
+ ") exceeds the configured maximum"
+ " (" + pSizeMax + ")",
pCount, pSizeMax);
=>
FileUploadException ex = new SizeLimitExceededException(
"服务器拒绝了您的请求,原因可能是向服务器提交的数据发生了丢失。", pCount, pSizeMax);
把914行~918行改一下。
throw new SizeLimitExceededException(
"the request was rejected because its size ("
+ requestSize
+ ") exceeds the configured maximum ("
+ sizeMax + ")",
=>
thrownewSizeLimitExceededException("服务器拒绝了您的请求,原因是提交数据量过大(通常是由于上传文件过大),请返回上页重试。"
+ " (最大字节数:" + sizeMax / 1024
+ "K)", requestSize, sizeMax);
再看一下第二个问题。
2、由于错误的产生,原来页面上输入的内容也全部不见了,也就是说ms注入失败。
关于这个问题我在javaeye上搜索到一篇文章(使用的commons-fileupload组件的jar包似乎比较老)。
http://www.javaeye.com/topic/197345
虽然按照此文,当上传失败时,能够将其他输入内容显示出来,但是这样做的结果是全部的文件肯定会上传到服务器上,也就是说,虽然是页面上报了文件因为太大,请求被拒绝的错,但是文件依然会被上传到服务器上,commons-fileupload组件根本没会去拦文件的上传。
在这里要说明一下,如果你不抛出这个异常,请求的流会继续向服务器上传,只有当整个流上传完了之后,commons-fileupload组件才能正确的分析出文件部分、文本部分。所以,在这里抛出异常是不得已的作法,如果不抛异常,后果是虽然页面报错,但文件还是会被传到服务器的上,这一步根本没挡住输入流的上传,如果没挡住的话,大家想想会有什么后果?
所以,综上所述,对于第二个问题,如果出现了这个异常,我们根本无法让原来输入的内容还显示出来的,因为commons-fileupload组件并没有解析全部的输入内容,直接给出异常了,到了ms拦截器中,request里就是空的,根本取不到meter,所以也就无法注入到Action中了。这种情况下,只能显示一个告知用户由于提交数据量过大,服务器拒绝了请求的错误信息,比较好的方法是,直接跳到一个专门的页面,提示用户,然后让用户点返回来再次输入,否则用户会感觉上传文件大就大吧,怎么连我输入的其他一些内容也没给保存住。当然,如果能用Ajax来上传文件,对客户的操作体验可能是最好的,但是,这样可能会导致服务器上有些挂空的文件(上传后从来没被用过),需要想法清除的。
整个分析下来,我们说第二个问题基本上是无法避免的。