PAYATHON 2026

<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();

检查以下结果:

  1. isPostback() 是否为 true。HTTP POST 与 JSF 回发的判断不同。
  2. viewId 是否仍是按钮所在的页面。
  3. 是否有代码提前要求渲染响应或结束请求。

如果恢复视图后直接进入 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 尚未调用时,修改导航规则通常无法解决问题。

备注:内容仅供参考。