【问题标题】:Why JSF calls getters multiple times为什么 JSF 多次调用 getter
【发布时间】:2014-09-01 01:30:14
【问题描述】:

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

感谢您的意见!

【问题讨论】:

    标签: performance jsf el getter


    【解决方案1】:

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

    这通常会在每个 JSF 请求-响应周期被调用一到两次,具体取决于组件是输入组件还是输出组件 (learn it here)。但是,当用于迭代 JSF 组件(例如 &lt;h:dataTable&gt; 和 &lt;ui:repeat&gt;)时,或者在诸如 rendered 属性之类的布尔表达式中使用时,此计数可能会上升(很多)。 JSF(特别是 EL)根本不会缓存 EL 表达式的计算结果,因为它可能在每次调用时返回不同的值(例如,当它依赖于当前迭代的数据表行时)。

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

    JSF 支持 bean 中的 Getter 方法应设计为仅返回已经准备好的属性,仅此而已,完全符合 Javabeans specification。他们根本不应该做任何昂贵的数据库/业务逻辑。为此,应使用 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),它可能会被多次调用。 p>

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

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

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

    另见:

    【讨论】:

    • 只是不要使用 getter 来做业务逻辑。就这样。重新排列代码逻辑。我敢打赌,它已经通过使用构造函数、构造函数或动作方法以聪明的方式解决了。
    • -1,强烈反对。 javaBeans 规范的全部要点是允许 属性不仅仅是一个字段值,并且动态计算的“派生属性”是完全正常的。担心多余的 getter 调用只不过是过早的优化。
    • 期待他们做的不仅仅是返回数据,因为你清楚地说明了自己:)
    • 您可以添加 getter 中的延迟初始化在 JSF 中仍然有效 :)
    • @Harry:它不会改变行为。但是,您可以通过延迟加载和/或通过 FacesContext#getCurrentPhaseId() 检查当前阶段 ID 有条件地处理 getter 中的任何业务逻辑。
    【解决方案2】:

    我还建议使用 Primefaces 之类的框架而不是股票 JSF,他们在 JSF 团队之前解决了这些问题。 g 在 primefaces 中你可以设置部分提交。否则 BalusC 已经很好地解释了它。

    【讨论】:

      【解决方案3】:

      原帖于PrimeFaces论坛@http://forum.primefaces.org/viewtopic.php?f=3&t=29546

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

      Getter 方法:

      PageNavigationController.getGmapsAutoComplete()
      

      被ui引用:包含在index.xhtml中

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

      请参阅下面的(原始)代码。

      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,则可以使用 Producers 方法。 它会被多次调用,但第一次调用的结果被缓存在 bean 的范围内,对于计算或初始化重对象的 getter 来说是有效的! 请参阅here,了解更多信息。

        【讨论】:

          【解决方案5】:

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

          安全检查可能很复杂,例如LDAP 查询等。所以你必须避免使用

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

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

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

          【讨论】:

          • 你可以将结果缓存在RequestParameterMap中
          【解决方案6】:

          我已经写了一篇article,关于如何使用 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>
          

          希望它会有所帮助!

          【讨论】:

            【解决方案7】:

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

            【讨论】:

              【解决方案8】:

              如果 someProperty 的值为 计算昂贵,这可以 可能是个问题。

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

              【讨论】:

              • 因此问题是——如果 someProperty 对应于计算成本高昂的东西(或者正如你所说的访问数据库或分解素数),那么避免每次请求多次计算的最佳方法是什么?我在问题中列出的最佳解决方案。如果您不回答这个问题,那么 cmets 是一个发帖的好地方,不是吗?此外,您的帖子似乎与您对 BalusC 帖子的评论相矛盾——在 cmets 中,您说即时计算是可以的,而在您的帖子中,您说这很愚蠢。我能问一下你的界限在哪里吗?
              • 这是一个滑动比例,而不是一个黑白问题。有些事情显然不是问题,例如添加一些值,因为它们花费的时间不到百万分之一秒(实际上要少得多)。有些显然是一个问题,例如数据库或文件访问,因为它们可能需要 10 毫秒或更长时间 - 你肯定需要知道这些,以便尽可能避免它们,而不仅仅是在吸气剂中。但对于其他一切,这条线是分析器告诉你的地方。
              【解决方案9】:

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

              【讨论】:

              • 你说的是这个Spring AOP吗?你知道我在哪里可以找到处理 Aspects 的代码 sn-p 或两个吗?阅读 Spring 文档的整个第 6 章似乎有点矫枉过正,因为我没有使用 Spring;)
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2017-07-07
              • 1970-01-01
              • 2011-05-15
              相关资源
              最近更新 更多