Чому JSF дзвонить отримувачам кілька разів


256

Скажімо, я вказую компонент outputText таким чином:

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

Якщо я друкую повідомлення журналу, коли somePropertyвиклик 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 таким чином?

Дякуємо за ваш внесок!

Відповіді:


340

Це викликано характером відкладених виразів #{}(зауважте, що "застарілі" стандартні вирази ${}поводяться точно так само, коли Facelets використовується замість JSP). Відкладений вираз не оцінюється відразу , а створюється як ValueExpressionоб'єкт, і метод getter за виразом виконується щоразу, коли код викликає ValueExpression#getValue().

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

Оцінка виразу EL та використання методу getter - це дуже дешева операція, тому взагалі про це не варто хвилюватися. Однак історія змінюється, коли ви використовуєте дорогу логіку DB / бізнесу в методі getter чомусь. Це буде повторно виконуватися кожного разу!

Методи 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;
    }

}

Зверніть увагу , що ви повинні НЕ використовувати конструктор або ініціалізацію блоку бін для роботи , тому що він може бути викликаний кілька разів , якщо ви використовуєте систему управління бобом , який використовує проксі, такі як CDI.

Якщо для вас дійсно немає інших способів, через деякі обмежувальні вимоги до дизайну, тоді вам слід ввести ледаче завантаження всередині методу геттера. Тобто, якщо властивість є null, то завантажте і призначте її, а інше поверніть її.

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

        return someProperty;
    }

Таким чином, дорога логіка БД / бізнесу не буде зайвою необхідністю виконуватись при кожному виклику доступу.

Дивитися також:


5
Просто не використовуйте гетерів для ділової логіки. Це все. Перестановіть логіку коду. Мою обставину, що це вже виправлено просто використанням конструктора, постконструкції чи методу дій розумним шляхом.
BalusC

3
-1, категорично не згоден. Вся суть специфікації javaBeans полягає в тому, щоб дозволити властивостям бути більше, ніж просто значення поля, а "похідні властивості", які обчислюються на льоту, є абсолютно нормальними. Турбуватися про надлишкові дзвінки на виклик є не що інше, як передчасна оптимізація.
Майкл Боргвардт

3
Очікуйте, якщо вони зроблять більше, ніж повертають дані, як ви так чітко заявили самі :)
BalusC,

4
Ви можете додати, що лінива ініціалізація в getters досі діє в 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;
}

}

Цей перехоплювач використовується у файлі конфігурації пружини:

    <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 був більше ГОРЯчим SPOT у Java Visual VM, ніж решта мій код (або більшість мого коду).

Метод Геттера:

PageNavigationController.getGmapsAutoComplete()

Посилання на ui: включити в index.xhtml

Нижче ви побачите, що PageNavigationController.getGmapsAutoComplete () - ГОРЯЧЕ SPOT (випуск продуктивності) у 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 () більше не ГОРЯЧИЙ SPOT в Java Visual VM (навіть навіть більше не з’являється)

Ділиться цією темою, оскільки багато хто з експертних користувачів порадили молодшим розробникам JSF НЕ додавати код у методах "getter". :)


4

Якщо ви використовуєте CDI, ви можете використовувати методи виробників. Він буде називатися багато разів, але результат першого дзвінка є кешованим в обсязі бобу і є ефективним для тих, хто обчислює або ініціалізує важкі предмети! Дивіться тут , для отримання додаткової інформації.


3

Ви, ймовірно, можете використовувати AOP для створення якогось аспекту, який кешував результати наших отримувачів протягом певного часу. Це не дозволить вам копіювати та вставляти код котла у десятки аксесуарів.


Це весняний AOP, про який ви говорите? Чи знаєте ви, де я можу знайти фрагмент коду чи два, що стосуються аспектів? Читання цілого 6-го розділу документації про Весну здається непосильним, оскільки я не використовую Весна;)
Севаш,

-1

Якщо значення деяких властивостей дорого обчислити, це потенційно може бути проблемою.

Це те, що ми називаємо передчасною оптимізацією. У рідкісному випадку, коли профайлер повідомляє вам, що обчислення властивості настільки надзвичайно дороге, що виклик його втричі, а не один раз має значний вплив на ефективність, ви додаєте кешування під час опису. Але якщо ви не зробите щось по-справжньому нерозумно, як факторинг-праймес або доступ до бази даних у геттера, ваш код, швидше за все, має десяток гірших неефективностей у місцях, про які ви ніколи не думали.


Звідси виникає питання - якщо деяка власність відповідає чомусь дорогому для обчислення (або коли ви отримуєте доступ до бази даних або факторинг-праймес), який найкращий спосіб уникнути проведення обчислень кілька разів за запит і чи є рішення, яке я вказав у питанні найкращий. Якщо ви не відповідаєте на запитання, коментарі - це гарне місце для публікації, ні? Крім того, ваш пост, здається, суперечить вашому коментарю до публікації BalusC - у коментарях ви говорите, що це добре робити обчислення на льоту, а у своєму дописі ви говорите, що це дурно. Чи можу я запитати, де ви проводите лінію?
Севас

Це розсувна шкала, а не чорно-біла проблема. Деякі речі очевидно не є проблемою, наприклад, додавання кількох значень, оскільки вони займають менше мільйонної частини секунди ( набагато менше, насправді). Деякі, очевидно, є проблемою, як, наприклад, доступ до БД або файлів, оскільки вони можуть зайняти 10 мс і довше - і ви, безумовно, повинні знати це, щоб ви могли їх уникнути, якщо це можливо, не лише в розпорядниках. Але для всього іншого, рядок - це те, де вам розповідає профайлер.
Майкл Боргвардт

-1

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


-2

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

Перевірка безпеки може бути складною, наприклад. LDAP-запит тощо. Тому ви повинні уникати цього

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

і ви повинні забезпечити протягом сеансу це за кожний запит.

Думаю, JSF повинен реалізувати тут деякі розширення, щоб уникнути декількох викликів (наприклад, анотація називає @Phase(RENDER_RESPONSE)цей метод лише один раз після RENDER_RESPONSEфази ...)


2
Ви можете кешувати результат у RequestParameterMap
Крістоф Руссі,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.