为什么JSF多次调用getter


256

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

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

如果在someProperty调用getter for时打印一条日志消息并加载页面,则很容易注意到每个请求多次调用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计算的值昂贵,则可能存在问题。

我在Google上搜索了一下,发现这是一个已知问题。一种解决方法是包括检查并查看是否已经计算出:

private String someProperty;

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

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

有什么替代方法?没有太多不必要的代码,有没有办法实现这一目标?有没有办法阻止JSF以这种方式运行?

感谢您的输入!

Answers:


340

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

通常,每个JSF请求-响应周期将调用一次或两次,具体取决于组件是输入组件还是输出组件(在此处学习)。但是,在迭代JSF组件(例如<h:dataTable><ui:repeat>)时,或者在诸如rendered属性之类的布尔表达式中使用此值时,此计数可能会增加(很多)。JSF(特别是EL)根本不会缓存EL表达式的评估结果,因为它在每次调用时可能返回不同的值(例如,当它依赖于当前迭代的数据表行时)。

评估EL表达式并调用getter方法是很便宜的操作,因此您通常根本不必担心这一点。但是,由于某种原因,当您在getter方法中执行昂贵的数据库/业务逻辑时,情况会发生变化。每次都会重新执行!

JSF支持bean中的getter方法应该按照这样的方式设计:它们完全返回 JavaBeans 规范,仅返回已经准备好的属性,仅此而已。他们根本不应该执行任何昂贵的DB /业务逻辑。为此,应使用bean @PostConstruct和/或(动作)侦听器方法。它们在基于请求的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的构造函数或初始化块的工作,因为它可能如果你使用它使用代理,如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调用中不必要地执行昂贵的DB /业务逻辑。

也可以看看:


5
只是不要使用吸气剂进行业务逻辑。就这样。重新排列您的代码逻辑。我敢打赌,仅通过聪明地使用构造函数,postconstruct或action方法就已经修复了它。
BalusC 2010年

3
-1,强烈不同意。javaBeans规范的全部要点是允许属性不仅仅是一个字段值,并且即时计算出的“派生属性”是完全正常的。担心冗余的getter调用只是过早的优化。
Michael Borgwardt 2010年

3
就像他们这么清楚地表明自己一样,期望他们做的还不只是返回数据:)
BalusC,2010年

4
您可以添加一点:getters中的延迟初始化在JSF中仍然有效:)
Bozho 2010年

2
@哈里:这不会改变行为。但是,您可以通过延迟加载和/或通过检查当前阶段ID来有条件地处理getter中的任何业务逻辑FacesContext#getCurrentPhaseId()
BalusC,

17

使用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 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>

希望对您有所帮助!


6

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

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

吸气方法:

PageNavigationController.getGmapsAutoComplete()

ui:include在index.xhtml中引用

在下面,您将看到PageNavigationController.getGmapsAutoComplete()是Java Visual VM中的一个热点(性能问题)。如果进一步往下看,在屏幕截图上,您将看到PrimeFaces惰性数据表getter方法getLazyModel()也是一个热点,仅当最终用户正在执行许多“惰性数据表”类型的工作/操作/任务时在应用程序中。:)

Java Visual VM:显示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中的HOT SPOT(甚至不再显示)

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


4

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


3

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


您正在谈论的是这个春季AOP吗?您知道在哪里可以找到一两个涉及Aspect的代码段吗?阅读Spring文档的整个第6章似乎
有些矫kill过

-1

如果someProperty的值计算起来很昂贵,则可能是一个问题。

这就是我们所说的过早优化。在极少数情况下,探查器会告诉您,属性的计算非常昂贵,以至于调用三次而不是一次会对性能产生重大影响,因此请按照您的描述添加缓存。但是,除非您做一些真正愚蠢的事情,例如质数分解或在吸气剂中访问数据库,否则您的代码很可能在从未想到的地方效率低下。


因此,存在一个问题-如果someProperty对应于某种计算昂贵的东西(或者您访问数据库或因数素数),那么避免每个请求多次进行计算的最佳方法是什么?这是我在问题中列出的解决方案最好的。如果您不回答问题,则评论是发布的好地方,不是吗?而且,您的帖子似乎与您对BalusC的帖子的评论相矛盾-在您认为即时进行计算的评论中,您在帖子中说它很愚蠢。我能问一下你在哪划界线吗?
塞瓦斯

这是一个浮动比例,而不是黑白问题。有些事情显然不是问题,例如添加一些值,因为它们花费的时间不到百万分之一秒(实际上得多)。显然,有些问题是一个问题,例如数据库或文件访问,因为它们可能需要10毫秒或更长时间-并且您肯定需要了解这些问题,以便在可能的情况下避免它们,而不仅仅是在吸气剂中。但是对于其他所有内容,该行都是分析器告诉您的位置。
Michael Borgwardt,2010年

-1

我还建议使用诸如Primefaces之类的Framework代替股票JSF,它们在JSF团队e之前解决了此类问题。g您可以将其设置为部分提交。否则,BalusC解释得很好。


-2

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

安全检查可能很复杂,例如。LDAP查询等。因此您必须避免使用

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

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

Ich认为JSF必须在此处实现一些扩展以避免重复调用(例如@Phase(RENDER_RESPONSE),在RENDER_RESPONSE阶段结束后仅对该方法进行一次注释调用)。


2
您可以将结果缓存在RequestParameterMap中
Christophe Roussy 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.