下文書(shū)中包的版本:commons-fileupload-1.2.1.jar、struts2-core-2.1.2.jar
孫鑫的書(shū)《Struts2 深入詳解》509頁(yè)是關(guān)于限制上傳文件的最大長(zhǎng)度的內(nèi)容。
其中談到fileUpload攔截器只是當(dāng)文件上傳到服務(wù)器上之后,才進(jìn)行的文件類型和大小判斷。
Struts2框架底層默認(rèn)用的是apache的commons-fileupload組件對(duì)上傳文件進(jìn)行接受處理。
通過(guò)struts.multipart.maxSize屬性來(lái)對(duì)文件大小進(jìn)行限定時(shí),將直接影響到commons-fileupload組件的文件大小設(shè)定,默認(rèn)是2M。當(dāng)上傳文件超過(guò)了這個(gè)尺寸時(shí),將從commons-fileupload組件中拋出SizeLimitExceededException異常。上傳文件攔截器捕獲到這個(gè)異常后,將直接把該異常信息設(shè)置為Action級(jí)別的錯(cuò)誤信息。
經(jīng)過(guò)我的測(cè)試和對(duì)源代碼的Debug,發(fā)現(xiàn)確實(shí)如孫鑫書(shū)中所言,如果上傳文件大于2M時(shí),在頁(yè)面上就出現(xiàn)了一堆英文的錯(cuò)誤信息,大致是:the request was rejected because its size....exceeds the configured maximum...并且在fieUpload中將來(lái)自MultiPartRequestWrapper型request對(duì)象的錯(cuò)誤信息給加到了Action的錯(cuò)誤中。
這時(shí)候,你在ApplicationResources.properties中自定義的上傳文件過(guò)大的錯(cuò)誤信息根本不起作用。原因就如書(shū)上所言,在底層commons-fileupload組件中就把異常給拋出來(lái)了文件根本沒(méi)被上傳,所以到了fileUpload攔截器時(shí),根據(jù)取不到文件,當(dāng)然也就沒(méi)法對(duì)文件的類型和大小進(jìn)行判斷了。
然而,這個(gè)異常直接帶來(lái)兩個(gè)問(wèn)題:
1、在頁(yè)面上顯示了英文的錯(cuò)誤信息。這樣的信息顯然不是我們想要的。
2、由于錯(cuò)誤的產(chǎn)生,原來(lái)頁(yè)面上輸入的其他文本內(nèi)容也都不見(jiàn)了,也就是說(shuō)params注入失敗。
帶著這兩個(gè)問(wèn)題,我們來(lái)探尋一下Struts2對(duì)于請(qǐng)求的處理過(guò)程。
注:這并不是一篇關(guān)于Struts2請(qǐng)求過(guò)程的介紹,主要是為了解決以上兩個(gè)問(wèn)題,才引起的簡(jiǎn)單分析。
首先當(dāng)然我們要拿FilterDispatcher開(kāi)刀。
在doFilter方法中調(diào)用了prepareDispatcherAndWrapRequest方法,為了包裝出Struts2自己的request對(duì)象,在prepareDispatcherAndWrapRequest方法中調(diào)用Dispatcher類的wrapRequest方法,在這個(gè)方法里,會(huì)根據(jù)請(qǐng)求內(nèi)容的類型(提交的是文本的,還是multipart/form-data格式),決定是使用tomcat的HttpServletRequestWrapper類分離出請(qǐng)求中的數(shù)據(jù),還是使用Struts2的MultiPartRequestWrapper來(lái)分離請(qǐng)求中的數(shù)據(jù)。
注:向服務(wù)器請(qǐng)求時(shí),數(shù)據(jù)是以流的形式向服務(wù)器提交,內(nèi)容是一些有規(guī)則東東,我們平時(shí)在jsp中用request內(nèi)置對(duì)象取parameter時(shí),實(shí)際上是由tomcat的HttpServletRequestWrapper類分解好了的,無(wú)需我們?cè)俜纸膺@些東西了。
當(dāng)然,在這里,我們研究的是上傳文件的情況,所以,由于form中設(shè)定的提交內(nèi)容是媒體格式的,所以,Dispatcher類的wrapRequest方法會(huì)將請(qǐng)求交由MultiPartRequestWrapper類來(lái)處理。
MultiPartRequestWrapper這個(gè)類是Struts2的類,并且繼承了tomcat的HttpServletRequestWrapper類,也是我們將用來(lái)代替HttpServletRequest這個(gè)類的類,看名字也知道,是對(duì)多媒體請(qǐng)求的包裝類。
Struts2本身當(dāng)然不會(huì)再造個(gè)輪子,來(lái)解析請(qǐng)求,而是交由Apache的commons-fileupload組件來(lái)解析了。
在MultiPartRequestWrapper的構(gòu)造方法中,會(huì)調(diào)用MultiPartRequest(默認(rèn)為JakartaMultiPartRequest類)的parse方法來(lái)解析請(qǐng)求。
在Struts2的JakartaMultiPartRequest類的parse方法中才會(huì)真正來(lái)調(diào)用commons-fileupload組件的ServletFileUpload類對(duì)請(qǐng)求進(jìn)行解析,至此,Struts2已經(jīng)實(shí)現(xiàn)了將請(qǐng)求轉(zhuǎn)交commons-fileupload組件對(duì)請(qǐng)求解析的全過(guò)程。剩下的就是等commons-fileupload組件對(duì)請(qǐng)求解析完畢后,拿到分解后的數(shù)據(jù),根據(jù)field名,依次將分解后的field名和值放到params(HashMap類型)里,同時(shí)JakartaMultiPartRequest類重置了HttpServletRequest的好多方法,比如熟知的getParameter、getParameterNames、getParameterValues,實(shí)際上都是從解析后得到的那個(gè)params對(duì)象里拿數(shù)據(jù),在這個(gè)過(guò)程,commons-fileupload組件也乖乖的把上傳的文件分析好了,JakartaMultiPartRequest也毫不客氣的把分解后的文件一個(gè)一個(gè)的放到了files(HashMap類型)中,實(shí)際上此時(shí),commons-fileupload組件已經(jīng)所有要上傳的文件上傳完了。至此,Struts2實(shí)現(xiàn)了對(duì)HttpServletRequest類的包裝,當(dāng)回到MultiPartRequestWrapper類后,再取一下上述解析過(guò)程中發(fā)生的錯(cuò)誤,然后把錯(cuò)誤加到了自己的errors列表中了。同樣我們會(huì)發(fā)現(xiàn)在MultiPartRequestWrapper類中,也把HttpServletRequest類的好多方法重載了,畢竟是個(gè)包裝類嘛,實(shí)際上對(duì)于上傳文件的請(qǐng)求,在Struts2后期的處理中用到的request都是MultiPartRequestWrapper類對(duì)象,比如我們調(diào)用getParameter時(shí),直接調(diào)用的是MultiPartRequestWrapper的getParameter方法,間接調(diào)的是JakartaMultiPartRequest類對(duì)象的getParameter方法。
注:從這里,我們就可以看出,JakartaMultiPartRequest是完全設(shè)計(jì)成可以替換的類了。
然后繼續(xù)向回返,到了Dispatcher類的wrapRequest方法,直接把MultiPartRequestWrapper對(duì)象返回了,我們就終于回到了FilterDispatcher類的prepareDispatcherAndWrapRequest方法,此時(shí),我們拿到了完全解析好了的request對(duì)象(MultiPartRequestWrapper類),該對(duì)象又進(jìn)一步被返回到了FilterDispatcher類的doFilter方法,也就是回到了出發(fā)點(diǎn),至此,doFilter中拿到的request對(duì)象就是一個(gè)將請(qǐng)求中的數(shù)據(jù)分解好的了HttpServletRequest對(duì)象,我們完全可以用getParameter方法取其中的數(shù)據(jù)了,同時(shí),我們也可以用getFiles得到文件數(shù)組了。
doFilter方法中,會(huì)進(jìn)一步調(diào)用actionMapper的getMapping方法對(duì)url進(jìn)行解析,找出命名空間和action名等,以備后面根據(jù)配置文件調(diào)用相應(yīng)的攔截器和action使用。
關(guān)于doFilter方法中下一步對(duì)Dispatcher類的serviceAction方法的調(diào)用,不再描述,總之在action被調(diào)用之前,會(huì)首先走到fileUpload攔截器(對(duì)應(yīng)的是FileUploadInterceptor類),在這個(gè)攔截器中,會(huì)先看一下request是不是 MultiPartRequestWrapper,如果不是,就說(shuō)明不是上傳文件用的request,fildUpload攔截器會(huì)直接將控制權(quán)交給下一個(gè)攔截器;如果是,就會(huì)把request對(duì)象強(qiáng)轉(zhuǎn)為MultiPartRequestWrapper對(duì)象,然后調(diào)用hasErrors方法,看看有沒(méi)有上傳時(shí)候產(chǎn)生的錯(cuò)誤,有的話,就直接加到了Action的錯(cuò)誤(Action級(jí)別的)中了。另外,在fileUpload攔截器中會(huì)將MultiPartRequestWrapper對(duì)象中放置的文件全取出來(lái),把文件、文件名、文件類型取出來(lái),放到request的parameters中,這樣到了params攔截器時(shí),就可以輕松的將這些內(nèi)容注入到Action中了,這也就是為什么fileUpload攔截器需要放在params攔截器前面的理由。在文件都放到request的parameters對(duì)象里之后,fileUpload攔截器會(huì)繼續(xù)調(diào)用其他攔截器直到Action等執(zhí)行完畢,他還要做一個(gè)掃尾的工作:把臨時(shí)文件夾中的文件刪除(這些文件是由commons-fileupload組件上傳的,供你在自己的Action中將文件copy到指定的目錄下,當(dāng)action執(zhí)行完了后,這些臨時(shí)文件當(dāng)然就沒(méi)用了)。
你好,你還在看嗎?呵呵,是不是太多了,也太亂了,沒(méi)辦法,Struts2就是這樣的調(diào)用的。也不知道Struts2有沒(méi)有公開(kāi)其Sequence圖,我是想畫(huà)一個(gè),不過(guò),太懶,還是看著代碼說(shuō)說(shuō)吧。
如果上面看煩了,也完全可以不看了,直接看下面的。
在上面一番分析之后,文件上傳的全過(guò)程就結(jié)束了。
我們回到我們的問(wèn)題上來(lái)。
先看第一個(gè):
1、在頁(yè)面上顯示了英文的錯(cuò)誤信息。這顯然不是我們想要的。
沒(méi)辦法了,commons-fileupload組件沒(méi)想到國(guó)際化,在FileUploadInterceptor攔載器中,也沒(méi)想著國(guó)際化,直接放到Action的錯(cuò)誤中了,就沒(méi)他事了,三種做法:
(1)在錯(cuò)誤顯示之前,把這條錯(cuò)誤給換掉,應(yīng)該難度不大,我沒(méi)做留給你做了。
(2)或者重寫(xiě)一下JakartaMultiPartRequest這個(gè)類,把捕捉到的異常信息換成自己的,然后,通過(guò)Struts2的配置文件,把我們重寫(xiě)的這個(gè)parser換上去用。
(3)直接改commons-fileupload組件的類,換成中文的。
我具體說(shuō)一下第(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(
"服務(wù)器拒絕了您的請(qǐng)求,原因可能是向服務(wù)器提交的數(shù)據(jù)發(fā)生了丟失。", pCount, pSizeMax);
把914行~918行改一下。
throw new SizeLimitExceededException(
"the request was rejected because its size ("
+ requestSize
+ ") exceeds the configured maximum ("
+ sizeMax + ")",
=>
throw new SizeLimitExceededException("服務(wù)器拒絕了您的請(qǐng)求,原因是提交數(shù)據(jù)量過(guò)大(通常是由于上傳文件過(guò)大),請(qǐng)返回上頁(yè)重試。"
+ " (最大字節(jié)數(shù):" + sizeMax / 1024
+ "K)", requestSize, sizeMax);
再看一下第二個(gè)問(wèn)題。
2、由于錯(cuò)誤的產(chǎn)生,原來(lái)頁(yè)面上輸入的內(nèi)容也全部不見(jiàn)了,也就是說(shuō)params注入失敗。
關(guān)于這個(gè)問(wèn)題我在javaeye上搜索到一篇文章(使用的commons-fileupload組件的jar包似乎比較老)。
http://www.javaeye.com/topic/197345
雖然按照此文,當(dāng)上傳失敗時(shí),能夠?qū)⑵渌斎雰?nèi)容顯示出來(lái),但是這樣做的結(jié)果是全部的文件肯定會(huì)上傳到服務(wù)器上,也就是說(shuō),雖然是頁(yè)面上報(bào)了文件因?yàn)樘螅?qǐng)求被拒絕的錯(cuò),但是文件依然會(huì)被上傳到服務(wù)器上,commons-fileupload組件根本沒(méi)會(huì)去攔文件的上傳。
在這里要說(shuō)明一下,如果你不拋出這個(gè)異常,請(qǐng)求的流會(huì)繼續(xù)向服務(wù)器上傳,只有當(dāng)整個(gè)流上傳完了之后,commons-fileupload組件才能正確的分析出文件部分、文本部分。所以,在這里拋出異常是不得已的作法,如果不拋異常,后果是雖然頁(yè)面報(bào)錯(cuò),但文件還是會(huì)被傳到服務(wù)器的上,這一步根本沒(méi)擋住輸入流的上傳,如果沒(méi)擋住的話,大家想想會(huì)有什么后果?
所以,綜上所述,對(duì)于第二個(gè)問(wèn)題,如果出現(xiàn)了這個(gè)異常,我們根本無(wú)法讓原來(lái)輸入的內(nèi)容還顯示出來(lái)的,因?yàn)閏ommons-fileupload組件并沒(méi)有解析全部的輸入內(nèi)容,直接給出異常了,到了params攔截器中,request里就是空的,根本取不到parameter,所以也就無(wú)法注入到Action中了。這種情況下,只能顯示一個(gè)告知用戶由于提交數(shù)據(jù)量過(guò)大,服務(wù)器拒絕了請(qǐng)求的錯(cuò)誤信息,比較好的方法是,直接跳到一個(gè)專門(mén)的頁(yè)面,提示用戶,然后讓用戶點(diǎn)返回來(lái)再次輸入,否則用戶會(huì)感覺(jué)上傳文件大就大吧,怎么連我輸入的其他一些內(nèi)容也沒(méi)給保存住。當(dāng)然,如果能用Ajax來(lái)上傳文件,對(duì)客戶的操作體驗(yàn)可能是最好的,但是,這樣可能會(huì)導(dǎo)致服務(wù)器上有些掛空的文件(上傳后從來(lái)沒(méi)被用過(guò)),需要想法清除的。
整個(gè)分析下來(lái),我們說(shuō)第二個(gè)問(wèn)題基本上是無(wú)法避免的。