<h:form> 未关联 UIForm,commandButton 的 action 为何未被调用?
结论
先检查浏览器提交的请求和服务器端组件树。UIForm.processDecodes() 断点未命中,说明问题可能发生在表单解码之前,但仅凭这一点,还不能断定 <h:form> 没有对应的 UIForm 实例,也不能认定是 MyFaces 2.3.7 的缺陷。
正常情况下,JSF 会先恢复视图,再解码表单和按钮。按钮在解码时识别到自己的提交参数,才会将 ActionEvent 加入事件队列,随后调用 action。这个过程中断时,页面仍可能重新渲染,看起来就像“只刷新了一下”。
现有代码片段还不足以确定根因。下一步需要确认浏览器实际提交了哪些参数,以及服务器在这次请求中恢复了哪些组件。
为什么现有测试还不能确定问题所在
按钮的 binding setter 被调用,只能证明 JSF 在构建或恢复组件时处理了这个绑定,无法证明按钮参与了本次请求的解码,或已经产生动作事件。
故意写错 action 方法名却没有报错,也可能是因为流程尚未执行到调用动作表达式的位置,因此还没有暴露方法名错误。
输入组件上的 immediate="true" 会把转换和校验提前到 Apply Request Values 阶段,Bean 属性的 setter 通常仍在 Update Model Values 阶段执行。如果校验失败,setter 和默认在 Invoke Application 阶段执行的 action 都可能不被调用。JSF 2.3 UIInput API
不过,校验失败通常无法解释同一次请求为何完全没有进入表单的 processDecodes(),所以需要先确认断点观察是否准确。
第一步:检查浏览器实际提交的表单
打开开发者工具的 Network 面板,保留请求记录,然后点击按钮,检查以下内容:
- 是否发出了 POST,请求地址是否由
FacesServlet处理。 - 是否先发生重定向,最终看到的其实是另一次 GET 请求。
- POST 参数中是否包含
javax.faces.ViewState。 - 是否包含表单标识,以及被点击按钮的完整
clientId。
在没有额外命名容器的普通页面中,参数通常类似:
owner_form=owner_form
owner_form:ownerPublicationOption=某个选项值
owner_form:startOwnerWorkflowButton=确定
javax.faces.ViewState=...
以生成页面中的实际 name 为准,不能只根据 XHTML 中的 id 推测。
标准表单渲染器通过表单的 clientId 参数判断表单是否已提交;标准按钮渲染器也需要识别按钮参数,才会将动作事件加入队列。表单解码规则、按钮解码规则
这两种情况需要分开检查:
- 缺少表单标识,可能导致
isSubmitted()为false,进而不处理子组件。 - 如果连
UIForm.processDecodes()的方法入口都未到达,就需要向前检查生命周期和组件树。
同时检查浏览器解析后的 DOM。在 Elements 面板选中按钮,再在控制台执行:
$0.form
返回值应是预期的表单。只检查当前文件中的标签,无法排除模板、页头或页脚输出了嵌套表单或不完整标签的情况。移除外层 <h:form> 后按钮失效,也不足以证明原页面最终生成的 HTML 结构正确。
第二步:确认请求是否进入 JSF 回发生命周期
在 Restore View 阶段结束后,可以直接在调试器中对以下表达式求值:
FacesContext context = FacesContext.getCurrentInstance();
context.isPostback();
context.getViewRoot().getViewId();
context.getRenderResponse();
context.getResponseComplete();
检查以下结果:
isPostback()是否为true。HTTP POST 与 JSF 回发的判断不同。viewId是否仍是按钮所在的页面。- 是否有代码提前要求渲染响应或结束请求。
如果恢复视图后直接进入 Render Response,表单就不会经历正常的解码流程。检查过滤器、URL 重写规则、登录拦截,以及项目中的 PhaseListener 或其他自定义生命周期逻辑。renderResponse() 会要求跳过后续未执行的阶段,responseComplete() 会要求结束当前生命周期。FacesContext API
还要确认断点对应的是服务器实际加载的类。遗留部署中的源码附件、应用携带的 JSF JAR 和容器提供的 JSF 实现可能不一致。如果断点设在错误的类或源码版本上,未命中就不能作为生命周期未执行的证据。
第三步:检查恢复后的组件树
检查提交请求在 Restore View 阶段结束后的组件树,不能只看首次打开页面时的对象。
从 FacesContext.getCurrentInstance().getViewRoot() 开始,检查 children 和 facets,找到 owner_form,并确认:
- 实际类型是否为
javax.faces.component.UIForm的子类。 - 按钮是否位于该表单的组件子树中。
- 表单和祖先组件在当前请求中的
isRendered()是否为true。 - 当前
clientId是否与浏览器提交的参数一致。
如果恢复后的组件树中没有表单,继续检查模板组合、动态 include、c:if 等构建时条件,以及创建或移除组件的自定义代码。首次渲染时存在的组件,在条件变化后的回发中未必会按相同结构恢复。确认组件树中确实缺少表单后,才有依据判断 <h:form> 是否未生成或未保留 UIForm。
此时还要检查标签实际使用的命名空间。对于 JSF 2.3 Facelets,标准声明是:
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
h 是前缀,标签含义由它绑定的 URI 决定。检查当前作用域内是否重新声明了该前缀,以及项目是否配置了自定义标签映射。JSF HTML 标签库定义
用独立页面隔离模板影响
新建一个不依赖原有页头、页脚和模板的诊断页面,通过项目现有的 FacesServlet 映射访问。沿用现有 Bean,暂时移除组件 binding 和输入组件上的 immediate,并显示全部校验消息:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<h:head>
<title>表单诊断</title>
</h:head>
<h:body>
<h:form id="owner_form">
<h:messages showSummary="true" showDetail="true" />
<h:selectOneRadio id="ownerPublicationOption"
layout="pageDirection"
required="true"
requiredMessage="请选择一个选项"
value="#{immoDetailBean.ownerPublicationOption}">
<f:selectItems value="#{valueListsBean.ownerOptionsList}" />
</h:selectOneRadio>
<h:commandButton id="startOwnerWorkflowButton"
value="确定"
action="#{immoDetailBean.startOwnerWorkflow}" />
</h:form>
</h:body>
</html>
诊断用的动作方法可以写成:
public String startOwnerWorkflow() {
System.out.println("startOwnerWorkflow");
return null;
}
先不选择选项,提交后观察是否出现必填提示;再选择一个有效选项提交,观察方法是否执行。这里的 return null 用于留在当前页面,不能用来验证预期的导航结果。
如果独立页面正常,再逐步恢复模板、页头、页脚和脚本。如果仍不正常,继续检查请求参数和生命周期,无须反复修改按钮表达式。
最后再处理校验与导航
表单进入解码、按钮产生动作事件后,再检查转换、校验和模型更新。<h:messages> 应显示所有消息;如果设置为只显示全局消息,可能会漏掉输入组件的错误。
给按钮添加 immediate="true" 不能作为通用修复。按钮的 immediate 会把动作处理提前到 Apply Request Values 阶段,此时输入值可能尚未更新到 Bean,业务方法可能读到旧值。按钮 immediate 的定义
确认 startOwnerWorkflow() 已执行后,再检查它返回的 outcome 是否与导航配置匹配。action 尚未调用时,修改导航规则通常无法解决问题。
备注:内容仅供参考。
