問題
前一陣子使用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=#{paramtype}>
<f:selectItem itemlabel=student itemvalue=student />
<f:selectItem itemlabel=teacher itemvalue=teacher />
</h:selectOneRadio>
<h:commandButton id=add action=#{backingBeanadd} />
</h:form>
編譯部署重新刷新頁面不錯B頁面上單選框的狀態能根據是否來自A頁面的鏈接呈現選中或否的狀態一切看上去都很美似乎已經完成了功能開發但是等等讓我們提交表單浏覽器刷新了一遍又回到了這個頁面通過檢查後台數據庫以及日志文件我們發現
數據庫裡面並沒有添加新的記錄
系統也沒有按照配置的navigation轉向正確的頁面
glassfish的日志文件中沒有add方法執行打印的日志也沒有任何異常信息這三點說明#{backingBeanadd}方法並沒有調用原來可以工作的添加功能出現了bugJSF在處理頁面提交請求的過程中發生了什麼?讓我們來調試一下
原則
在軟件開發中調試的目的是解決如何定位系統問題所在的問題一般意義上解決問題的原則套用胡適先生的話就是大膽假設小心求證套用《麥肯錫方法》則是以事實為基礎以假設為導向結構化推理具體來看調試是這樣一種分析問題的方法面對復雜的問題通過逐步確定正確或者錯誤的事情縮小問題范圍直到定位問題所在為止把事情確定化也可以細分為以下步驟
提出猜想
驗證猜想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注冊到facesconfigsxml文件裡面看整個請求過程發生了什麼?通過查看 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裡面是如何介紹UIInputupdateModel() 方法的
Call setValue() method of the ValueExpression to update the value that the ValueExpression points at問題轉移到javaxelValueExpression的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我們寫的是${paramkey}從文檔裡面可以得知param就是 externalContextgetRequestParameterMap()而 ExternalContextgetRequestParameterMap()的文檔描寫是這樣的
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=#{backingBeanadd}/>
</h:form>
部署運行不錯現在的頁面組件能保持選中的狀態也能順利創建新紀錄日志文件中也有add方式的執行信息說明的確是因為#{paramkey} 表達式的求值出錯導致異常這裡的#{param}已經不再是上一步的#{param}自然無法從externalContext的 RequestParameterMap裡面找到參數名為type的值因此JSF運行到這裡因為無法取到參數值去更新頁面的單選框組件所以就跳出了處理過程
現在回過頭來看一下問題的原因JSF在處理請求的時候會對頁面組件樹上的所有組件進行遞歸更新它會根據組件定義的EL表達式來重新計算值更新組件狀態以保證JSF頁面組件的狀態性我們得到的教訓是param等JSF隱含對象或許能用但最好不要放在JSF組件裡面進什麼廟拜什麼神我們還是選擇JSF推薦的backingbean來保持組件的值
結語
軟件調試是一項很有意思的活動常常給開發人員帶來解謎般的快感或者一團亂麻的糾結導入代碼設置斷點逐步調試並不是最好的辦法清楚地劃分問題域找准確定點可能會事半功倍當然在找出水面下面的暗礁之後別忘記給自己給其他人mark上這塊區域的暗礁位置能極大減少以後觸礁的痛苦
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26352.html