JSF中的'binding'属性是如何工作的?它应该在什么时候以及如何使用?

81

有很多材料可以区分JSF中的value属性和binding属性。

我对两种方法的区别很感兴趣。考虑以下情况:

public class User {
    private String name;
    private UICommand link;

    // Getters and setters omitted.
}
<h:form>
    <h:commandLink binding="#{user.link}" value="#{user.name}" />
</h:form>
当指定value属性时,发生的情况非常简单明了。getter运行以返回User bean的name属性值,并将该值打印到HTML输出中。
但是我不理解binding是如何工作的。生成的HTML如何与User bean的link属性保持绑定?
以下是手动美化和注释后生成的输出的相关部分(请注意id j_id_jsp_1847466274_1是自动生成的,且有两个隐藏的input小部件)。 我正在使用Sun的JSF RI版本1.2。
<form action="/TestJSF/main.jsf" enctype="application/x-www-form-urlencoded"
    id="j_id_jsp_1847466274_1" method="post"  name="j_id_jsp_1847466274_1">
    <input name="j_id_jsp_1847466274_1" type="hidden" value="j_id_jsp_1847466274_1">
    <a href="#" onclick="...">Name</a>
    <input autocomplete="off" id="javax.faces.ViewState" name="javax.faces.ViewState"
        type="hidden" value="-908991273579182886:-7278326187282654551">
</form>

这里的 binding 存储在哪里?

2个回答

138

它是如何工作的?

当构建/恢复 JSF 视图(Facelets/JSP 文件)时,将生成一个 JSF 组件树。在那个时刻,视图构建时间会评估所有 binding 属性(以及 id 属性和像 JSTL 这样的标签处理程序)。当需要在添加到组件树之前创建 JSF 组件时,JSF 将检查 binding 属性是否返回预先创建的组件(即非 null),如果是,则使用它。如果未预先创建,JSF 将“按常规方式”自动创建该组件,并调用 binding 属性后面的 setter 方法,参数为自动创建的组件实例。

实际上,它将组件实例的引用绑定到组件树中的一个作用域变量。这些信息在生成的HTML表示中并不可见。这些信息与生成的HTML输出无关。当表单被提交并且视图被恢复时,JSF组件树将从头开始重新构建,并且所有的binding属性将像上面的段落所描述的那样被重新评估。组件树重新创建后,JSF将将JSF视图状态恢复到组件树中。
组件实例是请求作用域的!重要的是要知道和理解具体的组件实例实际上是请求作用域的。它们在每个请求上都会被新创建,并且在恢复视图阶段从JSF视图状态中填充其属性值。因此,如果将组件绑定到后端bean的属性上,则后端bean的作用域绝对不能比请求作用域更广泛。参见JSF 2.0规范第3.1.5章节。
3.1.5 组件绑定
组件绑定通常与通过托管Bean创建设施(参见5.8.1节“VariableResolver和Default VariableResolver”)动态实例化的JavaBean一起使用。强烈建议应用程序开发人员将由组件绑定表达式指向的托管Bean放置在“请求”范围内。这是因为将其放置在会话或应用程序范围内将需要线程安全性,因为UIComponent实例依赖于在单个线程中运行。将组件绑定放置在“会话”范围内还可能对内存管理产生负面影响。
否则,组件实例将在多个请求之间共享,可能导致“duplicate component ID”错误和“奇怪”的行为,因为在视图中声明的验证器、转换器和侦听器会重新附加到先前请求的现有组件实例上。症状很明显:它们会随着每个请求在与组件绑定到的相同范围内执行多次,每次多一次。
而且,在重负载下(即当多个不同的HTTP请求(线程)同时访问和操作相同的组件实例时),你可能会面临应用程序崩溃,例如Stuck thread at UIComponent.popComponentFromEL,或者Threads stuck at 100% CPU utilization in HashMap during JSF saveState(),甚至是一些从JSF实现源代码直接出现的"奇怪"的IndexOutOfBoundsExceptionConcurrentModificationException,这发生在JSF忙于保存或恢复视图状态时(即堆栈跟踪指示saveState()restoreState()方法等)。
此外,由于单个组件基本上通过 `getParent()` 和 `getChildren()` 引用整个组件树的其余部分,因此当将单个组件绑定到视图或会话作用域的 bean 时,实际上是在 HTTP 会话中无效地保存整个 JSF 组件树。当视图中有相对较多的组件时,这将导致服务器可用内存成本高昂。
当然,通过在所有位置添加 `synchronized` 来使 JSF 组件线程安全可能可以解决所有问题,但它们从未打算在不同的浏览器选项卡/窗口/会话之间共享,因为这只会导致用户遇到 "wtf?" 的行为,并且会极大地降低性能。
在 bean 属性上使用 `binding` 是一种不良实践。
将整个组件实例绑定到bean属性,即使在请求范围的bean上也是在一个适当设计的JSF应用程序中相当罕见的用例,通常不是最佳实践。这表明设计存在问题。通常在视图端声明组件,并将其运行时属性(如`value`)以及其他属性(如`styleClass`、`disabled`、`rendered`等)绑定到普通的bean属性。然后,您只需操作您想要的那个bean属性,而不是获取整个组件并调用与属性关联的设置方法。
在需要根据静态模型进行“动态构建”组件的情况下,最好使用像JSTL这样的视图构建时标签,如果必要的话可以在标签文件中使用它,而不是使用createComponent()new SomeComponent()getChildren().add()等方法。另请参阅如何将旧的JSP代码段重构为一些JSF等效代码? 或者,如果一个组件需要基于动态模型进行“动态渲染”,那么只需使用一个迭代器组件<ui:repeat><h:dataTable>等)。另请参阅如何动态添加JSF组件

组合组件是一个完全不同的故事。将组件绑定到支持组件内部的<cc:implementation>是完全合法的(即通过<cc:interface componentType>标识的组件)。还可以参考 如何将java.util.Date拆分为表示小时和分钟的两个h:inputText字段,并使用f:convertDateTime如何使用JSF 2.0组合组件实现动态列表?

仅在局部范围内使用binding

然而,有时候你想要从一个特定的组件内部了解另一个组件的状态,这在与动作/值相关的验证用例中经常发生。为此,可以使用binding属性,但不能与bean属性结合使用。你只需要在binding属性中指定一个本地EL范围内唯一的变量名,如binding="#{foo}",然后在同一个视图中的其他地方,在渲染响应期间,该组件就可以直接通过#{foo}作为UIComponent引用来使用。以下是几个相关问题,其中答案中使用了这种解决方案: 但是我需要尽快修复一个现有的灾难性问题
如果您有一个现有的JSF应用程序,其中binding滥用地引用了一个范围大于请求的bean,并且您只是想以最短的时间来解决严重的内存和线程安全问题,那么您最好的选择是(正则表达式)查找并替换以下形式的getter和setter方法:
public SomeComponent getSomeComponent() {
    return someComponent;
}
public void setSomeComponent(SomeComponent someComponent) {
    this.someComponent = someComponent;
}

将以下内容转换为表单形式:
public SomeComponent getSomeComponent() {
    return getBoundComponent("someComponent");
}
public void setSomeComponent(SomeComponent someComponent) {
    setBoundComponent("someComponent", someComponent);
}

使用以下助手方法,基本上将它们保存在请求范围内:
protected static <C extends UIComponent> C getBoundComponent(String key) {
    return (C) getBoundComponents().get(key);
}
protected static <C extends UIComponent> void setBoundComponent(String key, C component) {
    getBoundComponents().put(key, component);
}
private static <C extends UIComponent> Map<String, C> getBoundComponents() {
    return (Map<String, C>) FacesContext.getCurrentInstance().getExternalContext().getRequestMap()
        .computeIfAbsent("com.example.BOUND_COMPONENTS", $ -> new HashMap<>());
}

然后让IDE自动删除未使用的字段。
另请参阅:
- [如何正确使用JSF中的组件绑定?(在会话范围的bean中使用请求范围的组件)](link1) - [视图范围:java.io.NotSerializableException: javax.faces.component.html.HtmlInputText](link2) - [绑定属性导致视图中找到重复的组件ID](link3)

@BalusC:您在回答中提到..当表单提交并恢复视图(POSTBACK)时,JSF组件树只是从头开始(REBUILT)。我有点困惑。由于视图只是UI组件树,在这种特定情况下是从初始GET请求中恢复的,它也必须提供该特定树结构。为什么需要再次重建POST请求呢?或者组件树在每个请求上都会重新构建吗?请详细说明。 - Farhan stands with Palestine
@BalusC:所以,当他们说视图状态需要被保留(有状态)以便处理生命周期时,这意味着只保存组件状态,而不是组件树。 - Farhan stands with Palestine
1
@Shirgill: 的确。UIViewRoot (基本上所有的 UIComponent)无法序列化。 - BalusC
@Shirgill:你们自己要对此负责。例如,在后置构造函数中使用component = new SomeComponent()。如果没有这样的构造函数,JSF会自动处理。 - BalusC
@BalusC:明白了。当我添加评论时,您在倒数第二个评论被编辑后的评论得到了反映。非常感谢。 - Farhan stands with Palestine
显示剩余13条评论

1
每个JSF组件都会将自己呈现为HTML,并完全控制其生成的HTML。 JSF可以使用许多技巧,具体使用哪些技巧取决于您使用的JSF实现。
  • 确保每个表单输入都有一个完全独特的名称,以便在将表单提交回呈现它的组件树时,很容易确定每个组件可以从哪里读取其值。
  • JSF组件可以生成javascript,将其提交回服务器,生成的javascript知道每个组件绑定的位置,因为它是由组件生成的。
  • 对于像hlink这样的东西,您可以在url中作为查询参数或url本身的一部分或作为matrx参数包含绑定信息。例如。

    http:..../somelink?componentId=123将允许jsf查看组件树,以查看链接123是否被点击。或者它可以是htp:..../jsf;LinkId=123

回答这个问题最简单的方法是创建一个只有一个链接的JSF页面,然后检查它生成的html输出。这样,您将精确了解使用您正在使用的JSF版本发生了什么。


我会说,我只在服务器端动态生成组件时使用组件绑定,设置所有属性,如“action”和“value”,然后让JSF框架完成其工作。 - Luiggi Mendoza
我将user存储为应用程序范围的托管bean,每次单击链接时,value="-908991273579182886:-7278326187282654551"中仅第二个数字更改,其他所有内容都相同。不知道这些操作有什么神奇之处。 - John Eipe

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接