Почему JSF вызывает геттеры несколько раз

Скажем, я указываю компонент outputText следующим образом:

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

Если я печатаю сообщение журнала, когда вызывается геттер для 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 дорого рассчитать, это может быть проблемой.

Я немного поработал с Google и понял, что это известная проблема. Одним из способов было включить проверку и посмотреть, была ли она уже рассчитана:

private String someProperty;

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

Основная проблема заключается в том, что вы получаете множество кода шаблонов, не говоря уже о частных переменных, которые вам могут не понадобиться.

Каковы альтернативы этому подходу? Есть ли способ достичь этого без лишнего кода? Есть ли способ остановить JSF таким образом?

Спасибо за ваш вклад!

Ответ 1

Это вызвано природой отложенных выражений #{} (обратите внимание, что "устаревшие" стандартные выражения ${} ведут себя точно так же, когда Facelets используется вместо JSP). Отложенное выражение вычисляется не сразу, а создается как объект ValueExpression, а метод getter, стоящий за выражением, выполняется каждый раз, когда код вызывает ValueExpression#getValue().

.Обычно он вызывается один или два раза за цикл запроса-ответа JSF, в зависимости от того, является ли компонент компонентом ввода или вывода (здесь вы можете узнать). Однако это число может возрасти (намного) выше, когда используется при итерации компонентов JSF (таких как <h:dataTable> и <ui:repeat>), или кое-где в логическом выражении, таком как атрибут rendered. JSF (в частности, EL) вообще не будет кэшировать вычисленный результат выражения EL, поскольку он может возвращать разные значения при каждом вызове (например, когда он зависит от текущей итерированной строки данных).

Оценка выражения EL и вызов метода-получателя - это очень дешевая операция, поэтому вам вообще не следует об этом беспокоиться. Однако история меняется, когда вы по какой-то причине выполняете дорогостоящую DB/бизнес-логику в методе getter. Это будет повторяться каждый раз!

Методы получения в компонентах поддержки JSF должны быть спроектированы таким образом, чтобы они возвращали уже подготовленное свойство и ничего более, в точности как в спецификации Javabeans. Они вообще не должны делать дорогую БД/бизнес-логику. Для этого следует использовать методы слушателя 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;
    }

}

Обратите внимание, что вы не должны использовать конструктор компонента или блок инициализации для задания, поскольку он может вызываться несколько раз, если вы используете среду управления компонентом, использующую прокси-серверы, такие как CDI.

Если для вас действительно нет других способов из-за некоторых ограничивающих требований к дизайну, то вы должны ввести ленивую загрузку в метод getter. То есть если свойство null, загрузите его и назначьте свойству, иначе верните его.

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

        return someProperty;
    }

Таким образом, дорогая БД/бизнес-логика не будет выполняться без необходимости при каждом вызове геттера.

Смотрите также:

Ответ 2

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

Ответ 3

Я написал статью о том, как кешировать JSF beans getter с Spring AOP.

Я создаю простой 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;
}

}

Этот перехватчик используется в файле конфигурации пружины:

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

Надеюсь, это поможет!

Ответ 4

Первоначально опубликовано на форуме PrimeFaces @http://forum.primefaces.org/viewtopic.php?f=3&t=29546.

Недавно я был одержим оценкой производительности моего приложения, настройкой JPA-запросов, заменой динамических SQL-запросов именованными запросами, и только сегодня утром я понял, что метод getter был скорее ГОРЯЧИМ СПОТОМ в Java Visual VM, чем остальные мой код (или большая часть моего кода).

Метод получения:

PageNavigationController.getGmapsAutoComplete()

Ссылка на пользовательский интерфейс: включить в index.xhtml

Ниже вы увидите, что 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() больше не является HOT SPOT в Java Visual VM (даже больше не отображается)

Разделяя эту тему, поскольку многие опытные пользователи посоветовали начинающим разработчикам JSF НЕ добавлять код в методы getter. :)

Ответ 5

Если вы используете CDI, вы можете использовать методы Producers. Он будет вызываться много раз, но результат первого вызова кэшируется в области bean и эффективен для геттеров, которые вычисляют или инициализируют тяжелые объекты! См. здесь, для получения дополнительной информации.

Ответ 6

Возможно, вы можете использовать AOP для создания своего рода Aspect, который кэшировал результаты наших геттеров для настраиваемого количества времени. Это может помешать вам копировать и вставлять шаблонный код в десятки аксессуаров.

Ответ 7

Если значение someProperty равно дорогостоящий расчет, это может потенциально может быть проблемой.

Это то, что мы называем преждевременной оптимизацией. В редком случае, когда профилировщик говорит вам, что вычисление свойства настолько необычайно дорого, что его трижды, а не один раз оказывает значительное влияние на производительность, вы добавляете кеширование, как вы описываете. Но если вы не делаете что-то действительно глупое, как факторинговые простые числа или получаете доступ к базе данных в геттере, ваш код, скорее всего, имеет десяток худших недостатков в тех местах, о которых вы никогда не думали.

Ответ 8

Я бы также посоветовал использовать такую платформу как Primefaces вместо стандартной JSF, они решают такие проблемы перед командой JSF e. g в простейших лицах вы можете установить частичную подачу. В остальном BalusC это хорошо объяснил.

Ответ 9

Это еще большая проблема в JSF. Fo, если у вас есть метод isPermittedToBlaBla для проверок безопасности, и в вашем представлении у вас есть rendered="#{bean.isPermittedToBlaBla}, тогда метод будет вызываться несколько раз.

Проверка безопасности может быть сложной, например. LDAP и т.д. Поэтому вы должны избегать этого с помощью

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

и вы должны обеспечить в течение сеанса bean для каждого запроса.

Ich думаю, что JSF должен реализовать здесь некоторые расширения, чтобы избежать множественных вызовов (например, аннотация @Phase(RENDER_RESPONSE) вызывать этот метод только один раз после фазы RENDER_RESPONSE)