问题
前一阵子使用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上这块区域的暗礁位置能极大减少以后触礁的痛苦