歡迎您光臨本站 註冊首頁

一次非典型性JSF調試過程

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0

問題

前一陣子使用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在處理頁面提交請求的過程中發生了什麼?讓我們來調試一下.

原則

在軟體開發中,調試的目的是解決「如何定位系統問題所在」的問題.一般意義上,解決問題的原則,套用胡適先生的話,就是「大膽假設,小心求證」;套用《麥肯錫方法》,則是「以事實為基礎,以假設為導向,結構化推理」.具體來看,調試是這樣一種分析問題的方法,面對複雜的問題,通過逐步確定正確或者錯誤的事情,縮小問題範圍,直到定位問題所在為止.把事情確定化,也可以細分為以下步驟:

提出猜想

驗證猜想or捕獲異常

提出新的猜想

在調試過程中,上面的步驟周而復始,並藉助於嚴密的邏輯論證來推動,直到定位最終的問題原因為止.同時,因為調試的過程中,開發人員面對的是已經「編碼完成」的系統.「編碼完成」的系統可以從如下兩個層面來看分解:

技術層面

業務層面

如何高效調試不僅僅是調試工具的問題,更是人對技術和業務領域的理解問題.在面對具體問題的時候,是採用「步步為營」,還是「分而治之」,都是依賴於當時的具體問題,以及開發人員對問題場景的理解程度和技術熟悉程度.那麼,高效地調試應該是什麼樣子呢?我覺得應該是這樣的:

劃定問題域邊界

選擇確定的出發點

藉助其他已經確定的點走查問題域,縮小問題域好,來看看針對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()方法或者事件JianTingQi裡面調用了FacesContext的renderResponse()方法,就會從事件隊列裡面直接清空剩下的事件,轉向Render Response步驟.但是我們沒有註冊任何的事件JianTingQi,也沒有自定義任何組件的 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上這塊區域的暗礁位置,能極大減少以後觸礁的痛苦.


[火星人 ] 一次非典型性JSF調試過程已經有730次圍觀

http://coctec.com/docs/java/show-post-60915.html