假设我像这样指定了一个outputText组件:

<h:outputText value="#{ManagedBean.someProperty}"/>

如果我打印一条日志消息,当someProperty的getter被调用并加载页面时,很容易注意到getter在每个请求中被调用了不止一次(在我的情况下发生了两次或三次):

DEBUG 2010-01-18 23:31:40,104 (ManagedBean.java:13) - Getting some property
DEBUG 2010-01-18 23:31:40,104 (ManagedBean.java:13) - Getting some property

如果someProperty的值计算起来很昂贵,这可能会成为一个问题。

我谷歌了一下,发现这是一个已知的问题。一个解决办法是包含一个检查,看看它是否已经计算过:

private String someProperty;

public String getSomeProperty() {
    if (this.someProperty == null) {
        this.someProperty = this.calculatePropertyValue();
    }
    return this.someProperty;
}

这样做的主要问题是您会得到大量的样板代码,更不用说您可能不需要的私有变量了。

这种方法的替代方案是什么?有没有一种方法可以在没有这么多不必要的代码的情况下实现这一点?是否有一种方法可以阻止JSF以这种方式行为?

谢谢你的建议!


当前回答

如果您正在使用CDI,您可以使用生产者方法。 它将被多次调用,但是第一次调用的结果缓存在bean的作用域中,对于正在计算或初始化沉重对象的getter来说非常有效! 更多信息请看这里。

其他回答

使用JSF 2.0,您可以将侦听器附加到系统事件

<h:outputText value="#{ManagedBean.someProperty}">
   <f:event type="preRenderView" listener="#{ManagedBean.loadSomeProperty}" />
</h:outputText>

或者,您可以将JSF页面包含在f:view标记中

<f:view>
   <f:event type="preRenderView" listener="#{ManagedBean.loadSomeProperty}" />

      .. jsf page here...

<f:view>

如果您正在使用CDI,您可以使用生产者方法。 它将被多次调用,但是第一次调用的结果缓存在bean的作用域中,对于正在计算或初始化沉重对象的getter来说非常有效! 更多信息请看这里。

您可以使用AOP来创建某种Aspect,在可配置的时间内缓存getter的结果。这将避免您需要在数十个访问器中复制并粘贴样板代码。

这是由延迟表达式#{}的性质引起的(注意,当使用Facelets而不是JSP时,“遗留”标准表达式${}的行为完全相同)。延迟的表达式不会立即求值,而是作为ValueExpression对象创建,并且在代码每次调用ValueExpression#getValue()时执行表达式后面的getter方法。

This will normally be invoked one or two times per JSF request-response cycle, depending on whether the component is an input or output component (learn it here). However, this count can get up (much) higher when used in iterating JSF components (such as <h:dataTable> and <ui:repeat>), or here and there in a boolean expression like the rendered attribute. JSF (specifically, EL) won't cache the evaluated result of the EL expression at all as it may return different values on each call (for example, when it's dependent on the currently iterated datatable row).

求值EL表达式并调用getter方法是一种非常廉价的操作,因此通常不需要担心这一点。但是,由于某种原因,当您在getter方法中执行昂贵的DB/业务逻辑时,情况就会发生变化。每次都会重新执行!

JSF支持bean中的Getter方法应该按照Javabeans规范的要求设计成只返回已经准备好的属性而不返回其他任何东西。他们根本不应该做任何昂贵的DB/业务逻辑。为此,应该使用bean的@PostConstruct和/或(action)侦听器方法。它们只在基于请求的JSF生命周期的某个点执行一次,这正是您想要的。

下面是所有预设/加载属性的正确方法的总结。

public class Bean {

    private SomeObject someProperty;

    @PostConstruct
    public void init() {
        // In @PostConstruct (will be invoked immediately after construction and dependency/property injection).
        someProperty = loadSomeProperty();
    }

    public void onload() {
        // Or in GET action method (e.g. <f:viewAction action>).
        someProperty = loadSomeProperty();
    }           

    public void preRender(ComponentSystemEvent event) {
        // Or in some SystemEvent method (e.g. <f:event type="preRenderView">).
        someProperty = loadSomeProperty();
    }           

    public void change(ValueChangeEvent event) {
        // Or in some FacesEvent method (e.g. <h:inputXxx valueChangeListener>).
        someProperty = loadSomeProperty();
    }

    public void ajaxListener(AjaxBehaviorEvent event) {
        // Or in some BehaviorEvent method (e.g. <f:ajax listener>).
        someProperty = loadSomeProperty();
    }

    public void actionListener(ActionEvent event) {
        // Or in some ActionEvent method (e.g. <h:commandXxx actionListener>).
        someProperty = loadSomeProperty();
    }

    public String submit() {
        // Or in POST action method (e.g. <h:commandXxx action>).
        someProperty = loadSomeProperty();
        return "outcome";
    }

    public SomeObject getSomeProperty() {
        // Just keep getter untouched. It isn't intented to do business logic!
        return someProperty;
    }

}

注意,您不应该为作业使用bean的构造函数或初始化块,因为如果您正在使用使用代理的bean管理框架(如CDI),那么它可能会被多次调用。

如果由于某些限制性的设计需求,确实没有其他方法,那么您应该在getter方法中引入延迟加载。例如,如果属性为空,则加载并将其分配给属性,否则返回它。

    public SomeObject getSomeProperty() {
        // If there are really no other ways, introduce lazy loading.
        if (someProperty == null) {
            someProperty = loadSomeProperty();
        }

        return someProperty;
    }

这样就不会在每个getter调用上都执行昂贵的DB/业务逻辑。

参见:

为什么getter被渲染的属性调用了这么多次? 在页面加载时调用JSF托管bean操作 我应该如何以及何时从数据库h:dataTable加载模型 如何填充选项h:selectOneMenu从数据库? 使用p:graphicImage和StreamedContent显示数据库中的动态图像 在JSF页面中定义和重用EL变量 在服务器请求之后测量JSF视图的呈现时间

我还建议使用这样的框架作为Primefaces,而不是库存JSF,它们可以在JSF团队之前解决这些问题,例如在Primefaces中你可以设置部分提交。除此之外,BalusC解释得很好。