Tag: debug

一次非典型性JSF调试过程

问题

前一阵子使用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的请求处理过程一共分成六个阶段:

  1. Restore View
  2. Apply Request Values
  3. Process Validations
  4. Update Model Values
  5. Invoke Application
  6. 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上这块区域的暗礁位置,能极大减少以后触礁的痛苦。