問題描述:
--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來上傳檔案,對客戶的操作體驗可能是最好的,但是,這樣可能會導緻伺服器上有些挂空的檔案(上傳後從來沒被用過),需要想法清除的。
整個分析下來,我們說第二個問題基本上是無法避免的。