[問題]
在解難經3:Struts2,攔截器攔不住Result?中,碰到的一個難題,當在PreResultListener中的拋出異常時,總是不能跳轉到配好的異常頁面去,而是拋出ServletException。換句話說,異常映射攔截器(具體來說指由XWork提供的ExceptionMappingInterceptor),根本攔截不住這種異常。按理說,不應該這樣啊,Action裏的異常是可以被捕捉並跳轉到相應的錯誤處理頁面的,到底是哪裏出的問題?
由於春節回湖南老家過年了,這個問題也暫時擱置下來。
[探幽]
過年在家的時候,腦袋裏經常回想到這個問題,初步的分析結果是,問題應該出在Struts2和XWork的核心類,以及異常映射攔截器的異常處理這幾個地方。
演員表
在分析這個問題之前,有必要介紹一下一些基本情況,下面就對分析過程中,幾個即將登場的核心類演員一一介紹(按出場順序排序),他們在Struts2的每次請求處理中,都扮演重要角色:
0、姓名:過濾分發器(FilterDispacher)
單位:Struts2
職責:初始化分發器,根據請求查找對應的Action映射配置,並調用分發器處理請求,執行Action
1、姓名:分發器(Dispacher)
單位:Struts2
部門:org.struts2.dispacher
職責:接受從過濾器分發過來的請求,並執行對應的Action
2、姓名:動作代理(ActionProxy、StrutsActionProxy)
單位:XWork(ActionProxy)、Struts2(StrutsActionProxy)
職責:負責維持Action配置,調用動作調用器
3、姓名:動作調用器(ActionInvocation、DefaultActionInvocation)
單位:XWork
職責:負責維持Action調用過程中的狀態,調用攔截器和最終的Action方法
4、姓名:攔截器(Interceptor、ExceptionMappingInterceptor)
單位:XWork
職責:負責在調用Action方法進行攔截,實現可擴展的功能處理,如異常映射攔截器實現異常時自動跳轉到錯誤頁面的功能
5、姓名:動作(Action)
單位:XWork
職責:負責最終的請求業務處理實現
6、姓名:結果(Result)
單位:XWork
職責:負責最終的響應頁面的生成
除了上述核心類做主角,在本次解難劇本中,下面的幾個跑龍套的傢伙也必不可少:
7、姓名:結果前置監聽器(PreResultListener、測試用匿名實現類)
單位:XWork(接口)、liuu(匿名測試監聽處理類實現)
職責:在Action執行之後,Result執行之前觸發該事件監聽器,方便應用做出特定的處理;在匿名實現類中,模擬拋出異常
8、姓名:測試攔截器(HelloInterceptor)
單位:liuu
職責:自定義攔截器實現,模擬在特定位置拋出異常
9、姓名:測試動作(HelloAction)
單位:liuu
職責:自定義Action實現,模擬某個業務請求處理
另外,請求(resquest)是貫穿全劇的道具。
接下來,我們佈置一下場景:
1、使用Struts2提供的空白項目war包(我用的是struts2-blank-2.0.11.war),導入到Eclipse中作爲測試項目
2、創建HelloInterceptor並配置爲hello攔截器,可以根據請求參數(如P1)在攔截器中拋出一個異常(E1),
3、在HelloInterceptor中,爲ActionInvocation增加一個匿名測試結果前置監聽器類,根據請求參數(P2)拋出異常(E2)
4、開發一個空的HelloAction類,配置默認攔截器棧和hello攔截器
6、開發一個正常業務頁面hello.jsp,並配置爲HelloAction的success結果頁面
5、開發一個錯誤顯示頁面error.jsp,並配置爲全局異常結果頁面error
好了,演員悉數登場,場景佈置完畢,好戲開鑼。
場景一:正常處理請求
場景描述:
1、在Tomcat下啓動項目,打開URL:http://localhost:8080/struts2-blank-2.0.11/example/hello.action
2、正常顯示hello.jsp頁面
執行分析:在請求得到正常處理時,各大旦角各司其責,配合默契,整體非常流暢:
從這個時序圖裏,我們可以看出,真正的主角,是動作調用器(DefaultActionInvocation),中心方法是invoke:
1、依次遍歷調用Action的攔截器棧
2、在棧底,執行Action
3、如果沒有執行過(executed,默認爲false),則執行:
3.1 查詢是否有結果前置監聽器,如果有則順序調用
3.2 如果需要執行過結果Result(默認爲true),則執行之
3.3 設置爲已執行狀態(executed=true)(注意這一點,這是理解本次難經的關鍵)
場景二、動作執行異常
場景描述:
1、在Tomcat下啓動項目,打開URL:http://localhost:8080/struts2-blank-2.0.11/example/hello.action?action
2、action參數觸發HelloAction拋出異常,最終跳轉到error.jsp頁面
場景分析:
如果對場景一理解清楚了,那麼,對動作(Action)執行異常時,所發生的一切,應該不難了解:
1、HelloAction拋出異常時,由於裏層的攔截器沒有catch,因此攔截器層層退棧
2、退到ExceptionMappingInterceptor後異常被catch,在查詢全局異常映射配置後,返回調用結果爲error
3、後續處理同場景一
場景三:監聽器調用異常
場景描述:
1、在Tomcat下啓動項目,打開URL:http://localhost:8080/struts2-blank-2.0.11/example/hello.action?inner
2、inner參數將觸發HelloInterceptor中的匿名PreResultListener拋出異常
3、但是最終卻顯示500錯誤,ServletException異常,而不是跳轉到error.jsp頁面
場景分析:
現在,問題來了。既然在Action中拋出異常時,可以自動跳轉到錯誤頁面,爲什麼結果前置監聽器裏拋出的異常時,不能跳轉到error.jsp,而是拋出ServletException呢?
來看看奧妙在哪裏吧:
發現問題在哪了麼:
關鍵的問題是,在PreResultListener拋出異常後,PreResultListener又被多執行了一次!
我們來看看關鍵代碼,截取自DefaultActionInvocation.invoke:
if (!executed) {
if (preResultListeners != null) {
for (Iterator iterator = preResultListeners.iterator();
iterator.hasNext();) {
PreResultListener listener = (PreResultListener) iterator.next();
String _profileKey="preResultListener: ";
try {
UtilTimerStack.push(_profileKey);
listener.beforeResult(this, resultCode);
}
finally {
UtilTimerStack.pop(_profileKey);
}
}
}
// now execute the result, if we're supposed to
if (proxy.getExecuteResult()) {
executeResult();
}
executed = true;
}
原來:
1、在第一次調用PreResultListener時,第一個異常拋出,當前線程退棧;執行不到“executed = true ",execute是false
2、在ExceptionMappingInterceptor捕捉到這個異常後,返回結果碼error
3、線程繼續退棧,直到退出所有攔截器,然後會重新執行上述代碼
4、由於execute還是false,所有PreResultListener被再次執行,於是又拋出第二個異常
5、對於這個新的異常,ExceptionMappingInterceptor已經無能爲力,直到Dispachter再次捕捉到這個異常,並轉爲ServletException拋出
6、Servlet容器catch了這個異常,轉到默認異常頁面,上面顯示的就是Tomcat的默認異常頁面
。。。。。。
唉,問題已經完全清楚了,或許,這應該算Struts2(這裏是2.0.11)的一個bug,不知道最新版裏是否有修正。
[解難]
搞清楚了原因,解決起來那就是手到擒來了:只要讓PreResultListener不能再次執行,一切就OK了。
我給匿名PreResultListener實現類增加一個狀態字段executed,防止其多次執行:
final ActionProxy ap = ai.getProxy();
ai.addPreResultListener(new PreResultListener() {
private boolean executed = false;
public void beforeResult(ActionInvocation invocation,
String resultCode) {
if (!executed) {
if (req.getParameter("fixed") != null)
executed = true;
System.out.println("in pre result listener ");
if (req.getParameter("inner") != null) {
throw new RuntimeException(
"exception in intercept befor result in inner class : "
+ req.getParameter("inner"));
}
}
}
});
場景四:解難
場景描述:
再次部署後,打開URL:http://localhost:8080/struts2-blank-2.0.11/example/hello.action?inner&fixed
霍霍,瀏覽器乖乖的跳轉到了error.jsp頁面,整個世界清靜了......
場景分析:
如果列爲看官大大能堅持看到這裏,不妨再看一下解難場景下的時序圖:
這應該纔是難經3:Struts2,攔截器攔不住Result?問題的最終解。