難經4:Struts2,攔截器攔不住的異常?!

[問題]

在解難經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?問題的最終解。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章