为什么 JSF 多次调用 getter

Why JSF calls getters multiple times

提问人:Sevas 提问时间:1/19/2010 最后编辑:BalusCSevas 更新时间:4/17/2022 访问量:105337

问:

假设我指定了一个 outputText 组件,如下所示:

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

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

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 以这种方式行事?

感谢您的输入!

性能 JSF el getter

评论


答:

3赞 matt b 1/19/2010 #1

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

评论

0赞 Sevas 1/19/2010
你说的是这个Spring AOP吗?你知道在哪里可以找到一两个处理 Aspects 的代码片段吗?阅读 Spring 文档的整个第 6 章似乎有点矫枉过正,因为我没有使用 Spring ;)
359赞 BalusC 1/19/2010 #2

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

这通常会在每个 JSF 请求-响应周期内调用一到两次,具体取决于该组件是输入组件还是输出组件(在此处了解)。但是,当用于迭代 JSF 组件(例如 和 )时,或者在布尔表达式(如属性)中,此计数可能会(大大)更高。JSF(特别是 EL)根本不会缓存 EL 表达式的计算结果,因为它可能会在每次调用时返回不同的值(例如,当它依赖于当前迭代的数据表行时)。<h:dataTable><ui:repeat>rendered

计算 EL 表达式和调用 getter 方法是一种非常便宜的操作,因此您通常完全不必担心这一点。但是,当您出于某种原因在 getter 方法中执行昂贵的 DB/业务逻辑时,情况会发生变化。每次都会重新执行!

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

以下是预设/加载属性的所有不同正确方法的摘要。

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 的构造函数或初始化块用于作业,因为如果您使用的是使用代理(如 CDI)的 Bean 管理框架,则可能会多次调用它。

如果由于一些限制性设计要求,你真的没有其他方法,那么你应该在 getter 方法中引入延迟加载。即,如果属性是 ,则加载并将其分配给属性,否则返回它。null

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

        return someProperty;
    }

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

另请参阅:

评论

5赞 BalusC 1/19/2010
只是不要使用 getter 来执行业务逻辑。就这样。重新排列代码逻辑。我敢打赌,只需以聪明的方式使用构造函数、postconstruct 或 action 方法,它就已经修复了。
3赞 Michael Borgwardt 1/19/2010
-1,强烈反对。javaBeans 规范的全部意义在于允许属性不仅仅是一个字段值,并且动态计算的“派生属性”是完全正常的。担心冗余的 getter 调用只不过是过早的优化。
3赞 BalusC 1/19/2010
期待他们是否像您如此明确地表示自己那样返回数据:)
4赞 Bozho 1/19/2010
您可以添加 getter 中的延迟初始化在 JSF 中仍然有效:)
2赞 BalusC 5/17/2011
@Harry:它不会改变行为。但是,您可以通过延迟加载和/或通过检查当前阶段 ID 来有条件地处理 getter 中的任何业务逻辑。FacesContext#getCurrentPhaseId()
-1赞 Michael Borgwardt 1/19/2010 #3

如果 someProperty 的值为 计算成本高昂,这可以 可能是一个问题。

这就是我们所说的过早优化。在极少数情况下,探查器会告诉您属性的计算非常昂贵,以至于调用它三次而不是一次会对性能产生重大影响,您可以按照描述添加缓存。但是,除非你做了一些非常愚蠢的事情,比如分解素数或在 getter 中访问数据库,否则你的代码很可能在你从未想过的地方有十几个更糟糕的低效率。

评论

0赞 Sevas 1/19/2010
因此,问题来了 - 如果 someProperty 对应于计算成本高昂的东西(或者如您所说的那样访问数据库或因式分解素数),那么避免每个请求多次进行计算的最佳方法是什么,我在问题中列出的解决方案是最好的解决方案吗?如果你没有回答这个问题,评论是一个发帖的好地方,不是吗?此外,您的帖子似乎与您对 BalusC 帖子的评论相矛盾——在评论中,您说即时进行计算是可以的,而在您的帖子中,您说这很愚蠢。我能问问你在哪里划线吗?
0赞 Michael Borgwardt 1/19/2010
这是一个滑动比例,而不是一个非黑即白的问题。有些事情显然不是问题,例如添加一些值,因为它们花费的时间不到百万分之一秒(实际上要少得多)。有些显然是一个问题,比如数据库或文件访问,因为它们可能需要 10 毫秒或更长时间——你肯定需要知道这些,这样你就可以尽可能避免它们,而不仅仅是在 getter 中。但对于其他所有内容,行是探查器告诉您的位置。
18赞 César Alforde 10/31/2010 #4

使用 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赞 Nicolas Labrot 1/5/2011 #5

我写了一篇关于如何使用 Spring AOP 缓存 JSF bean 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>

希望能有所帮助!

-2赞 Morad 1/6/2012 #6

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

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

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

并且您必须确保在会话 Bean 中每个请求都这样做。

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

评论

2赞 Christophe Roussy 4/19/2012
您可以将结果缓存在 RequestParameterMap 中
4赞 Mehdi 6/11/2012 #7

如果您使用的是 CDI,则可以使用 Producers 方法。 它将被调用很多次,但第一次调用的结果被缓存在 Bean 的范围内,并且对于正在计算或初始化重对象的 getter 来说是有效的! 有关详细信息,请参阅此处

6赞 Howard 4/14/2013 #8

引用自 PrimeFaces forum @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

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

吸气剂方法:

PageNavigationController.getGmapsAutoComplete()

由 index.xhtml 中的 ui:include 引用

下面,您将看到 PageNavigationController.getGmapsAutoComplete() 是 Java Visual VM 中的热点(性能问题)。如果你再往下看,在屏幕截图上,你会看到 getLazyModel(),PrimeFaces 惰性数据表的 getter 方法,也是一个热点,只有当最终用户在应用程序中做大量“懒惰数据表”类型的东西/操作/任务时。 :)

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' 方法中添加代码。:)

-1赞 Martin Karari #9

我还建议使用诸如 Primefaces 之类的框架而不是库存 JSF,它们在 JSF 团队之前解决了此类问题,例如在 primefaces 中,您可以设置部分提交。否则,BalusC已经很好地解释了它。