为什么JSF会多次调用getter方法

270

假设我像下面这样指定一个 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以这种方式行事?
谢谢您的参与!
9个回答

358
这是由延迟表达式 #{} 的特性引起的(请注意,当使用 Facelets 而非 JSP 时,“传统”的标准表达式 ${} 表现完全相同)。延迟表达式不会立即求值,而是作为 ValueExpression 对象创建,并且每次代码调用 ValueExpression#getValue() 时都会执行表达式后面的 getter 方法。
这通常会在每个 JSF 请求-响应周期中调用一到两次,具体取决于组件是输入组件还是输出组件(在这里了解更多)。但是,当在迭代 JSF 组件(例如 <h:dataTable><ui:repeat>)中使用时,或者在布尔表达式中(例如 rendered 属性),此计数可能会变得更高(很多)。JSF(具体来说是 EL)不会缓存 EL 表达式的计算结果,因为它可能在每次调用时返回不同的值(例如,当它依赖于当前迭代的 datatable 行时)。
评估EL表达式并调用getter方法是一种非常便宜的操作,因此通常不需要担心这个问题。但是,当您在getter方法中执行昂贵的数据库/业务逻辑时,情况就会发生变化。这将每次重新执行!JSF后端bean中的getter方法应该设计为仅返回已准备好的属性,而不做任何昂贵的数据库/业务逻辑,正如Javabeans规范所述。对于此类操作,应使用bean的@PostConstruct和/或(action)listener方法。它们在基于请求的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方法中引入延迟加载。即,如果属性为null,则加载并将其分配给该属性,否则返回它。
    public SomeObject getSomeProperty() {
        // If there are really no other ways, introduce lazy loading.
        if (someProperty == null) {
            someProperty = loadSomeProperty();
        }

        return someProperty;
    }

这样一来,昂贵的数据库/业务逻辑就不会在每次getter调用时不必要地执行。

另请参阅:


5
只需不要使用Getter来执行业务逻辑即可。重新排列您的代码逻辑。我打赌只需聪明地使用构造函数、PostConstruct或动作方法,问题就已经解决了。 - BalusC
3
强烈不同意。JavaBeans规范的全部意图就是允许属性不仅仅是一个字段值,而且计算出的“派生属性”是非常正常的。担心getter调用冗余只是过早优化。 - Michael Borgwardt
3
如你所清楚地表述,如果它们做得更多,不仅返回数据,那就请期待吧 :) - BalusC
4
你可以加上,在JSF中,使用getter进行延迟初始化仍然是有效的 :) - Bozho
2
@Harry:它不会改变行为。但是你可以通过延迟加载和/或检查当前阶段ID(使用FacesContext#getCurrentPhaseId())在getter中有条件地处理任何业务逻辑。 - BalusC
显示剩余12条评论

18

在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>

9
我已经写了一篇关于如何使用Spring AOP缓存JSF beans getter的文章
我创建了一个简单的MethodInterceptor,拦截所有带有特定注释的方法:
public class CacheAdvice implements MethodInterceptor {

private static Logger logger = LoggerFactory.getLogger(CacheAdvice.class);

@Autowired
private CacheService cacheService;

@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {

    String key = methodInvocation.getThis() + methodInvocation.getMethod().getName();

    String thread = Thread.currentThread().getName();

    Object cachedValue = cacheService.getData(thread , key);

    if (cachedValue == null){
        cachedValue = methodInvocation.proceed();
        cacheService.cacheData(thread , key , cachedValue);
        logger.debug("Cache miss " + thread + " " + key);
    }
    else{
        logger.debug("Cached hit " + thread + " " + key);
    }
    return cachedValue;
}


public CacheService getCacheService() {
    return cacheService;
}
public void setCacheService(CacheService cacheService) {
    this.cacheService = cacheService;
}

}

这个拦截器在Spring配置文件中使用:

    <bean id="advisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="pointcut">
        <bean class="org.springframework.aop.support.annotation.AnnotationMatchingPointcut">
            <constructor-arg index="0"  name="classAnnotationType" type="java.lang.Class">
                <null/>
            </constructor-arg>
            <constructor-arg index="1" value="com._4dconcept.docAdvance.jsfCache.annotation.Cacheable" name="methodAnnotationType" type="java.lang.Class"/>
        </bean>
    </property>
    <property name="advice">
        <bean class="com._4dconcept.docAdvance.jsfCache.CacheAdvice"/>
    </property>
</bean>

希望它有所帮助!

6

最初发布于PrimeFaces论坛@ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

最近,我一直在痴迷于评估我的应用程序的性能,调整JPA查询,用命名查询替换动态SQL查询,而且就在今天早上,我意识到一个getter方法在Java Visual VM中比我的其他代码(或大部分代码)更成为热点。

Getter方法:

PageNavigationController.getGmapsAutoComplete()

在index.xhtml中,ui:include引用了以下内容:

如下所示,PageNavigationController.getGmapsAutoComplete()是Java Visual VM中的一个热点(性能问题)。如果您进一步查看屏幕截图,您会发现getLazyModel(),即PrimeFaces懒惰数据表格获取器方法,也是一个热点,仅当最终用户在应用程序中执行大量“懒惰数据表格”类型的操作/任务时。

Java Visual VM: showing HOT SPOT

请见下方(原始)代码。
public Boolean getGmapsAutoComplete() {
    switch (page) {
        case "/orders/pf_Add.xhtml":
        case "/orders/pf_Edit.xhtml":
        case "/orders/pf_EditDriverVehicles.xhtml":
            gmapsAutoComplete = true;
            break;
        default:
            gmapsAutoComplete = false;
            break;
    }
    return gmapsAutoComplete;
}

在 index.xhtml 中被以下内容引用:

<h:head>
    <ui:include src="#{pageNavigationController.gmapsAutoComplete ? '/head_gmapsAutoComplete.xhtml' : (pageNavigationController.gmaps ? '/head_gmaps.xhtml' : '/head_default.xhtml')}"/>
</h:head>

解决方案:由于这是一个“getter”方法,移动代码并在调用方法之前分配值给gmapsAutoComplete;请参见下面的代码。
/*
 * 2013-04-06 moved switch {...} to updateGmapsAutoComplete()
 *            because performance = 115ms (hot spot) while
 *            navigating through web app
 */
public Boolean getGmapsAutoComplete() {
    return gmapsAutoComplete;
}

/*
 * ALWAYS call this method after "page = ..."
 */
private void updateGmapsAutoComplete() {
    switch (page) {
        case "/orders/pf_Add.xhtml":
        case "/orders/pf_Edit.xhtml":
        case "/orders/pf_EditDriverVehicles.xhtml":
            gmapsAutoComplete = true;
            break;
        default:
            gmapsAutoComplete = false;
            break;
    }
}

测试结果:PageNavigationController.getGmapsAutoComplete() 在 Java Visual VM 中不再是热点(甚至不再显示)

分享此主题,因为许多专家用户已建议初级 JSF 开发人员不要在“getter”方法中添加代码。:)


4
如果您正在使用CDI,可以使用生产者方法。它将被多次调用,但是第一次调用的结果会被缓存在bean的范围内,并且对于计算或初始化重量级对象的getter方法非常高效!有关更多信息,请参见此处

3

你可以使用AOP创建某种切面,来缓存我们的getter方法的结果,缓存时间可以配置。这样就不需要在数十个访问器中复制粘贴样板代码。


你说的是Spring AOP吗?你知道我能在哪里找到处理切面的一两个代码片段吗?阅读整个Spring文档第6章似乎有点过度杀伤力,因为我不使用Spring ;) - Sevas

-1
如果一些属性的值计算起来很昂贵,那么这可能会成为一个问题。这就是我们所说的过早优化。在极少数情况下,分析器告诉你某个属性的计算非常昂贵,调用它三次而不是一次对性能产生了重大影响,你可以像描述的那样添加缓存。但是,除非你做了某些真正愚蠢的事情,比如因式分解质数或在getter中访问数据库,否则你的代码很可能在你从未想过的地方存在许多更糟糕的效率低下问题。

因此,问题是 - 如果某个属性对应于计算昂贵的东西(或者像你所说的访问数据库或分解质数),那么避免在每个请求中多次进行计算的最佳方法是什么,我在问题中列出的解决方案是最好的吗?如果您不回答问题,评论是发布的好地方,不是吗?另外,您的帖子似乎与您在BalusC的帖子上的评论相矛盾 - 在评论中,您说即时计算是可以的,在您的帖子中,您说这很愚蠢。我可以问一下您划定的界限在哪里? - Sevas
这是一个滑动比例,而不是非黑即白的问题。有些事情显然不是问题,例如添加几个值,因为它们只需要不到一百万分之一秒(实际上远远少于这个时间)。有些明显是问题,例如数据库或文件访问,因为它们可能需要10毫秒或更长时间 - 你绝对需要知道这些,以便在可能的情况下避免它们,而不仅仅是在getter中。但对于其他所有情况,界限就在分析器告诉你的地方。 - Michael Borgwardt

-1
我建议使用Primefaces这样的框架,而不是原始的JSF,因为它们在JSF团队之前解决了这些问题,例如在primefaces中,您可以设置部分提交。否则,BalusC已经解释得很好了。

-2

这在JSF中仍然是一个大问题。例如,如果您有一个用于安全检查的方法isPermittedToBlaBla,并且在视图中有rendered="#{bean.isPermittedToBlaBla},那么该方法将被多次调用。

安全检查可能很复杂,例如LDAP查询等。因此,您必须避免这种情况。

Boolean isAllowed = null ... if(isAllowed==null){...} return isAllowed?

你必须确保在会话Bean中每个请求都是独立的。

我认为JSF在这里必须实现一些扩展来避免多次调用(例如注释@Phase(RENDER_RESPONSE)只在RENDER_RESPONSE阶段之后调用此方法一次...)


2
你可以将结果缓存到RequestParameterMap中。 - Christophe Roussy

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