它是如何工作的?
当构建/恢复 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实现源代码直接出现的"奇怪"的
IndexOutOfBoundsException
或
ConcurrentModificationException
,这发生在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)
UIViewRoot
(基本上所有的UIComponent
)无法序列化。 - BalusCcomponent = new SomeComponent()
。如果没有这样的构造函数,JSF会自动处理。 - BalusC