为什么JSF会多次调用getter

假设我像这样指定了一个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以这种方式行为?

谢谢你的建议!

103753 次浏览

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

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

这通常会在每个JSF请求-响应周期中调用一到两次,这取决于组件是输入组件还是输出组件(在这里学习)。然而,在迭代JSF组件(如<h:dataTable><ui:repeat>)中使用时,或者在像rendered属性这样的布尔表达式中使用时,这个计数可能会增加(多得多)。JSF(具体来说,EL)根本不会缓存EL表达式的计算结果,因为它五月在每次调用时返回不同的值(例如,当它依赖于当前迭代的数据表行时)。

求值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方法中引入延迟加载。例如,如果属性为null,则加载并将其赋值给属性,否则返回它。

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


return someProperty;
}

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

参见:

如果someProperty的值为 昂贵的计算,这可以

这就是我们所说的过早优化。在极少数情况下,如果分析器告诉您某个属性的计算非常昂贵,以至于调用它三次(而不是一次)会产生显著的性能影响,那么您就可以按照您的描述添加缓存。但是,除非您做了一些非常愚蠢的事情,如分解质数或在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>

我已经写了一个关于如何用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>

希望对大家有所帮助!

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

安全检查可能会很复杂。LDAP查询等。所以你必须避免

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

您必须在会话bean中确保每个请求都有此功能。

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

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

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

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

Getter方法:

PageNavigationController.getGmapsAutoComplete()

由ui:include在index.xhtml中引用

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

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中的热点(甚至不再显示)

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

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