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

256

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

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

Если я распечатываю сообщение журнала, когда somePropertyвызывается метод get и загружаю страницу, легко заметить, что метод get вызывается более одного раза за запрос (в моем случае это произошло два или три раза):

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 таким образом?

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

Sevas
источник

Ответы:

340

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

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

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

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

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

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

        return someProperty;
    }

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

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

BalusC
источник
5
Просто не используйте геттеры для ведения бизнес-логики. Вот и все. Перестройте свой код логики. Держу пари, что это уже исправлено, просто используя умный способ конструктора, постконструктора или метода действия.
BalusC
3
-1, сильно не согласен. Весь смысл спецификации javaBeans состоит в том, чтобы позволить свойствам быть больше, чем просто значение поля, и «производные свойства», которые вычисляются на лету, совершенно нормальны. Беспокойство по поводу избыточных вызовов геттеров - не что иное, как преждевременная оптимизация.
Майкл Боргвардт
3
Ожидайте, что они сделают больше, чем просто
вернут
4
Вы можете добавить, что ленивая инициализация в методах получения все еще действует в JSF :)
Божо
2
@ Гарри: это не изменит поведение. Однако вы можете обработать любую бизнес-логику в получателе условно, отложив загрузку и / или проверив текущий идентификатор фазы с помощью 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

Я написал статью о том, как кэшировать геттер JSF-компонентов с помощью 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;
}

}

Этот перехватчик используется в файле конфигурации 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, чем остальные. мой код (или большая часть моего кода).

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

PageNavigationController.getGmapsAutoComplete()

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

Ниже вы увидите, что PageNavigationController.getGmapsAutoComplete () является горячей точкой (проблемой производительности) в Java Visual VM. Если вы посмотрите дальше вниз, на снимке экрана вы увидите, что getLazyModel (), метод получения ленивых данных в PrimeFaces, также является горячей точкой, только когда конечный пользователь выполняет много «ленивых данных» типа вещей / операций / задач в приложении. :)

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

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

Говард
источник
4

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

Heidarzadeh
источник
3

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

Мэтт Б
источник
Вы говорите об этой весенней АОП? Вы знаете, где я могу найти фрагмент кода или два, касающиеся Аспектов? Чтение всей шестой главы документации Spring кажется излишним, поскольку я не использую Spring;)
Севас
-1

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

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

Майкл Боргвардт
источник
Отсюда вопрос - если someProperty соответствует чему-то дорогостоящему для вычисления (или, как вы говорите, обращаясь к базе данных или к простым факторингам), каков наилучший способ избежать вычисления несколько раз за запрос, и является ли решение, которое я перечислил в этом вопросе, следующим: лучший. Если вы не отвечаете на вопрос, комментарии - хорошее место для публикации, нет? Кроме того, ваш пост, кажется, противоречит вашему комментарию к сообщению BalusC - в комментариях вы говорите, что можно делать вычисления на лету, а в своем сообщении вы говорите, что это глупо. Могу я спросить, где вы проводите черту?
Сев
Это скользящая шкала, а не черно-белый вопрос. Некоторые вещи явно не являются проблемой, например, добавление нескольких значений, потому что они занимают менее миллионной доли секунды (на самом деле, намного меньше). Некоторые из них, очевидно, представляют собой проблему, например, доступ к БД или файлу, поскольку они могут занимать 10 мс или более - и вам обязательно нужно знать их, чтобы вы могли избежать их, если это возможно, не только в методах получения. Но для всего остального, линия - то, где профилировщик говорит Вам.
Майкл Боргвардт
-1

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

Мартин Карари
источник
-2

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

Проверка безопасности может быть сложной, например. LDAP-запрос и т. Д. Так что вы должны избегать этого с

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

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

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

Morad
источник
2
Вы можете кэшировать результат в RequestParameterMap
Кристоф Русси,