Posted on 2009-09-28 02:01
mingj 閱讀(3895)
評論(1) 編輯 收藏 所屬分類:
軟件調試
問題
前一陣子使用JSF開發web應用程序的過程中,碰到一個需求:A頁面上存在一個鏈接,用戶點擊鏈接會被重定向B頁面。頁面B上存在一個單選框,如果是通過A頁面的鏈接過來,會把單選框置為“選擇”的狀態。這是非常典型的頁面轉向,根據JSF的頁面轉向配置,以及對JSF隱含對象param的介紹,下面的代碼“貌似”可行:
A頁面:
<h:commandLink value="Add" action="add">
<f:param name="type" value="student" />
</h:commandLink>
B頁面:
<h:form>
<h:selectOneRadio id="type" value="#{param.type}">
<f:selectItem itemlabel="student" itemvalue="student" />
<f:selectItem itemlabel="teacher" itemvalue="teacher" />
</h:selectOneRadio>
<h:commandButton id="add" action="#{backingBean.add}" />
</h:form>
編譯、部署、重新刷新頁面。不錯,B頁面上單選框的狀態能根據是否來自A頁面的鏈接呈現選中或否的狀態:一切看上去都很美,似乎已經完成了功能開發。但是,等等,讓我們提交表單。瀏覽器刷新了一遍,又回到了這個頁面。通過檢查后臺數據庫以及日志文件,我們發現:
- 數據庫里面并沒有添加新的記錄
- 系統也沒有按照配置的navigation轉向正確的頁面
- glassfish的日志文件中沒有add方法執行打印的日志,也沒有任何異常信息
這三點說明,#{backingBean.add}方法并沒有調用,原來可以工作的添加功能出現了bug。JSF在處理頁面提交請求的過程中發生了什么?讓我們來調試一下。
原則
在軟件開發中,調試的目的是解決“如何定位系統問題所在”的問題。一般意義上,解決問題的原則,套用胡適先生的話,就是“大膽假設,小心求證”;套用《麥肯錫方法》,則是“以事實為基礎,以假設為導向,結構化推理”。具體來看,調試是這樣一種分析問題的方法,面對復雜的問題,通過逐步確定正確或者錯誤的事情,縮小問題范圍,直到定位問題所在為止。把事情確定化,也可以細分為以下步驟:
在調試過程中,上面的步驟周而復始,并借助于嚴密的邏輯論證來推動,直到定位最終的問題原因為止。同時,因為調試的過程中,開發人員面對的是已經“編碼完成”的系統。“編碼完成”的系統可以從如下兩個層面來看分解:
如何高效調試不僅僅是調試工具的問題,更是人對技術和業務領域的理解問題。在面對具體問題的時候,是采用“步步為營”,還是“分而治之”,都是依賴于當時的具體問題,以及開發人員對問題場景的理解程度和技術熟悉程度。那么,高效地調試應該是什么樣子呢?我覺得應該是這樣的:
- 劃定問題域邊界
- 選擇確定的出發點
- 借助其他已經確定的點走查問題域,縮小問題域
好,來看看針對JSF的這個問題如何調試。
步驟
我們先來劃定我們初始的問題域:JSF請求提交后,JSF不能正常調用后臺方法進行處理。我們想知道,JSF處理請求過程中哪個地方出問題了。那么我們確定的點是什么呢?JSF規范。因為我們使用的是SUN開發的JSF RI,所以它必然滿足JSF規范。在規范中,JSF的請求處理過程一共分成六個階段:
- Restore View
- Apply Request Values
- Process Validations
- Update Model Values
- Invoke Application
- Render Response
我們可以定義一個PhaseListener,注冊到faces-configs.xml文件里面,看整個請求過程發生了什么?通過查看 glassfish的日志文件,我們發現update model values之后就直接render response,沒有 invoke application。 如果一切正常,應該是從第一步執行到第六步,但現在跳過了第五步,直接從第四步到了第六步,是哪里出現了問題?好,從“JSF的處理過程”到“第四步 Update Model Values”,我們已經縮小了問題域的范圍,現在確定的點已經有JSF規范和 “Update Model Values”了。繼續,從JSF規范對步驟“”中尋找“Update Model Values”的說明:
If any of the updateModel() methods that was invoked, or an event listener that processed a queued event, called renderResponse() on the FacesContext instance for the current request, clear the remaining events from the event queue and transfer control to the Render Response phase of the request processing lifecycle. Otherwise, control must proceed to the Invoke Application phase.
這里提到如果我們在updateModel()方法或者事件監聽器里面調用了FacesContext的renderResponse()方法,就會從事件隊列里面直接清空剩下的事件,轉向Render Response步驟。但是我們沒有注冊任何的事件監聽器,也沒有自定義任何組件的 updateModel()方法,那就只能是在系統組件的updateModel()方法里面拋出異常被JSF引擎捕獲,然后直接 render response。現在進一步縮小范圍了,讓我們來看看Javaapi doc里面是如何介紹UIInput.updateModel() 方法的。
Call setValue() method of the ValueExpression to update the value that the ValueExpression points at.
問題轉移到javax.el.ValueExpression的setValue()方法,我們來看看這個方法的API:
Evaluates the expression relative to the provided context, and sets the result to the provided value.
Throws:
PropertyNotFoundException - if one of the property resolutions failed because a specified variable or property does not exist or is not readable.
再來看看組件的ValueExpression,我們寫的是“${param.key}”,從文檔里面可以得知param就是 externalContext.getRequestParameterMap(),而 ExternalContext.getRequestParameterMap()的文檔描寫是這樣的:
Return an immutable Map whose keys are the set of request parameters names included in the current request, and whose values (of type String) are the first (or only) value for each parameter name returned by the underlying request.
因為表單提交時的request跟之前頁面轉向時的Request肯定不是一樣,那是否由于該ValueExpression導致的問題。讓我們來驗證一下,把B頁面上單選框組件的值改成字符串字面值“student”。現在B頁面的單選框組件就變成了:
<h:form>
<h:selectOneRadio id="type" value="student">
<f:selectItem itemLabel="student" itemValue="student"/>
<f:selectItem itemLabel="teacher" itemValue="teacher"/>
</h:selectOneRadio>
<h:commandButton id="add" action="#{backingBean.add}"/>
</h:form>
部署,運行。不錯,現在的頁面組件能保持選中的狀態,也能順利創建新紀錄,日志文件中也有add方式的執行信息:說明的確是因為#{param.key} 表達式的求值出錯導致異常。這里的#{param}已經不再是上一步的#{param},自然無法從externalContext的 RequestParameterMap里面找到參數名為type的值。因此,JSF運行到這里,因為無法取到參數值去更新頁面的單選框組件,所以就跳出了處理過程。
現在,回過頭來看一下問題的原因:JSF在處理請求的時候,會對頁面組件樹上的所有組件進行遞歸更新,它會根據組件定義的EL表達式來重新計算值,更新組件狀態,以保證JSF頁面組件的狀態性。我們得到的教訓是param等JSF隱含對象或許能用,但最好不要放在JSF組件里面。“進什么廟,拜什么神”,我們還是選擇JSF推薦的backingbean來保持組件的值。
結語
軟件調試是一項很有意思的活動,常常給開發人員帶來解謎般的快感,或者一團亂麻的糾結。導入代碼、設置斷點、逐步調試并不是最好的辦法,清楚地劃分問題域,找準確定點可能會事半功倍。當然,在找出水面下面的暗礁之后,別忘記給自己、給其他人mark上這塊區域的暗礁位置,能極大減少以后觸礁的痛苦。