Dlaczego JSF dzwoni do pobierających wiele razy

256

Powiedzmy, że określam składnik outputText w następujący sposób:

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

Jeśli wydrukuję komunikat dziennika po somePropertywywołaniu modułu pobierającego dla i załaduję stronę, trywialne jest zauważenie, że moduł wywołujący jest wywoływany więcej niż raz na żądanie (dwa lub trzy razy tak jest w moim przypadku):

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

Jeśli somePropertyobliczenie wartości jest kosztowne, może to stanowić problem.

Trochę googlowałem i pomyślałem, że to znany problem. Jednym z obejść było sprawdzenie i sprawdzenie, czy zostało już obliczone:

private String someProperty;

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

Główny problem polega na tym, że dostajesz mnóstwo kodu typu „kocioł”, nie wspominając o zmiennych prywatnych, których możesz nie potrzebować.

Jakie są alternatywy dla tego podejścia? Czy istnieje sposób na osiągnięcie tego bez niepotrzebnego kodu? Czy istnieje sposób, aby powstrzymać JSF przed zachowaniem się w ten sposób?

Dzięki za wkład!

Sevas
źródło

Odpowiedzi:

340

Jest to spowodowane charakterem odroczonych wyrażeń #{}(zwróć uwagę, że standardowe wyrażenia „starsze” ${}zachowują się dokładnie tak samo, gdy zamiast JSP użyto Faceletów). Odroczone wyrażenie nie jest natychmiast analizowane, ale jest tworzone jako ValueExpressionobiekt, a metoda pobierająca wyrażenie jest wykonywana za każdym razem, gdy wywoływany jest kod ValueExpression#getValue().

Zwykle będzie to wywoływane jeden lub dwa razy na cykl JSF żądanie-odpowiedź, w zależności od tego, czy składnik jest składnikiem wejściowym czy wyjściowym ( dowiedz się tutaj ). Jednak ta liczba może wzrosnąć (znacznie), gdy jest używana w iteracji komponentów JSF (takich jak <h:dataTable>i <ui:repeat>), lub tu i tam w wyrażeniu boolowskim, takim jak renderedatrybut. JSF (konkretnie EL) w ogóle nie buforuje obliczonego wyniku wyrażenia EL, ponieważ może zwracać różne wartości przy każdym wywołaniu (na przykład, gdy jest zależny od aktualnie iterowanego wiersza danych).

Ocena wyrażenia EL i wywołanie metody gettera to bardzo tania operacja, więc ogólnie nie powinieneś się tym przejmować. Jednak historia zmienia się, gdy z jakiegoś powodu wykonujesz kosztowną logikę DB / biznesową w metodzie gettera. Zostanie to ponownie wykonane za każdym razem!

Getter w JSF podporowych fasoli powinny być zaprojektowane w taki sposób, że tylko powrót już przygotowaną własność i nic więcej, dokładnie jak na specyfikacji JavaBeans . Nie powinny w ogóle robić drogiej logiki DB / biznesowej. W tym celu @PostConstructnależy zastosować metody detektora komponentu bean i / lub (action). Są one wykonywane tylko raz w pewnym momencie cyklu życia JSF opartego na żądaniach i właśnie tego chcesz.

Oto podsumowanie wszystkich różnych właściwych sposobów wstępnego ustawiania / ładowania właściwości.

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

}

Należy pamiętać, że należy nie używać konstruktora lub bloku inicjalizacji fasoli do pracy, ponieważ może to być wywoływane wielokrotnie jeśli używasz ramy zarządzania fasola, który używa proxy, takie jak CDI.

Jeśli naprawdę nie ma dla ciebie innych sposobów, z powodu pewnych restrykcyjnych wymagań projektowych, powinieneś wprowadzić leniwe ładowanie w metodzie gettera. To znaczy null, jeśli właściwość jest , a następnie załaduj i przypisz ją do właściwości, w przeciwnym razie zwróć ją.

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

        return someProperty;
    }

W ten sposób kosztowna logika DB / biznes nie będzie niepotrzebnie wykonywana przy każdym wywołaniu gettera.

Zobacz też:

BalusC
źródło
5
Po prostu nie używaj getterów do robienia logiki biznesowej. To wszystko. Zmień układ logiki kodu. Założę się, że jest to już naprawione przez inteligentne użycie konstruktora, postkonstrukcji lub metody działania.
BalusC
3
-1, zdecydowanie się nie zgadzam. Cały punkt specyfikacji javaBeans polega na tym, aby właściwości były czymś więcej niż tylko wartością pola, a „właściwości pochodne” obliczane w locie są całkowicie normalne. Martwienie się o nadmiarowe wywołania gettera to przedwczesna optymalizacja.
Michael Borgwardt,
3
Spodziewaj się, że robią coś więcej niż zwracanie danych, tak jak sam to powiedziałeś :)
BalusC
4
można dodać, że leniwa inicjalizacja w
modułach pobierających
2
@Harry: To nie zmieni zachowania. Można jednak warunkowo obsłużyć dowolną logikę biznesową w module pobierającym, opóźniając ładowanie i / lub sprawdzając bieżący identyfikator fazy za pomocą FacesContext#getCurrentPhaseId().
BalusC
17

Dzięki JSF 2.0 możesz dołączyć detektor do zdarzenia systemowego

<h:outputText value="#{ManagedBean.someProperty}">
   <f:event type="preRenderView" listener="#{ManagedBean.loadSomeProperty}" />
</h:outputText>

Alternatywnie możesz umieścić stronę JSF w f:viewtagu

<f:view>
   <f:event type="preRenderView" listener="#{ManagedBean.loadSomeProperty}" />

      .. jsf page here...

<f:view>
César Alforde
źródło
9

Napisałem artykuł o tym, jak buforować moduł pobierający ziarna JSF za pomocą Spring AOP.

Tworzę prosty, MethodInterceptorktóry przechwytuje wszystkie metody opatrzone adnotacją specjalną adnotacją:

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

}

Ten przechwytywacz jest używany w wiosennym pliku konfiguracyjnym:

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

Mam nadzieję, że to pomoże!

Nicolas Labrot
źródło
6

Pierwotnie opublikowane na forum PrimeFaces @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

Ostatnio miałem obsesję na punkcie oceny wydajności mojej aplikacji, dostrajania zapytań JPA, zastępowania dynamicznych zapytań SQL zapytaniami nazwanymi, i właśnie dziś rano zrozumiałem, że metoda gettera była bardziej GORĄCĄ SPOTĄ w Java Visual VM niż reszta mój kod (lub większość mojego kodu).

Metoda Gettera:

PageNavigationController.getGmapsAutoComplete()

Odwołany przez ui: include in w index.xhtml

Poniżej zobaczysz, że PageNavigationController.getGmapsAutoComplete () to HOT SPOT (problem z wydajnością) w Java Visual VM. Jeśli spojrzysz w dół, na zrzucie ekranu zobaczysz, że getLazyModel (), leniwa metoda pobierania danych PrimeFaces, jest również gorącym punktem, tylko wtedy, gdy użytkownik wykonuje wiele „leniwych danych” typu rzeczy / operacji / zadań w aplikacji. :)

Java Visual VM: pokazuje HOT SPOT

Zobacz (oryginalny) kod poniżej.

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

Przywoływane przez następujące w index.xhtml:

<h:head>
    <ui:include src="#{pageNavigationController.gmapsAutoComplete ? '/head_gmapsAutoComplete.xhtml' : (pageNavigationController.gmaps ? '/head_gmaps.xhtml' : '/head_default.xhtml')}"/>
</h:head>

Rozwiązanie: ponieważ jest to metoda „pobierająca”, przenieś kod i przypisz wartość do gmapsAutoComplete przed wywołaniem metody; patrz kod poniżej.

/*
 * 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;
    }
}

Wyniki testu: PageNavigationController.getGmapsAutoComplete () nie jest już GORĄCYM SPOTEM w Java Visual VM (już się nawet nie pokazuje)

Udostępniam ten temat, ponieważ wielu ekspertów ekspertów radziło młodszym programistom JSF, aby NIE dodawali kodu w metodach „getter”. :)

Howard
źródło
4

Jeśli używasz CDI, możesz użyć metod producentów. Będzie wywoływany wiele razy, ale wynik pierwszego wywołania jest buforowany w zakresie komponentu bean i jest skuteczny dla osób pobierających, które obliczają lub inicjalizują ciężkie obiekty! Zobacz tutaj , aby uzyskać więcej informacji.

Heidarzadeh
źródło
3

Prawdopodobnie możesz użyć AOP do stworzenia jakiegoś aspektu, który buforuje wyniki naszych programów pobierających przez konfigurowalny okres czasu. Zapobiegnie to konieczności kopiowania i wklejania kodu szablonu na dziesiątkach akcesoriów.

matowy b
źródło
Czy chodzi o wiosenny AOP, o którym mówisz? Czy wiesz, gdzie mogę znaleźć fragment kodu lub dwa dotyczące aspektów? Czytanie całego szóstego rozdziału dokumentacji wiosennej wydaje się przesadą, ponieważ nie używam wiosny;)
Sevas
-1

Jeśli wartość niektórych własności jest kosztowna do obliczenia, może to stanowić problem.

Nazywamy to przedwczesną optymalizacją. W rzadkim przypadku, gdy profilujący mówi ci, że obliczenie właściwości jest tak wyjątkowo kosztowne, że trzykrotne jej wywołanie zamiast jednego ma znaczący wpływ na wydajność, dodajesz buforowanie, tak jak to opisano. Ale jeśli nie zrobisz czegoś naprawdę głupiego, takiego jak faktoryzacja liczb pierwszych lub uzyskiwanie dostępu do bazy danych w module pobierającym, Twój kod najprawdopodobniej ma kilkanaście gorszych nieefektywności w miejscach, o których nigdy nie pomyślałeś.

Michael Borgwardt
źródło
Stąd pytanie - jeśli someProperty odpowiada czemuś kosztownemu do obliczenia (lub po umieszczeniu go w bazie danych lub liczbach faktoringowych), jaki jest najlepszy sposób na uniknięcie wykonywania obliczeń kilka razy na żądanie i czy rozwiązaniem jest wymienione w pytaniu najlepszy. Jeśli nie odpowiadasz na pytanie, komentarze są dobrym miejscem do publikowania, nie? Ponadto twój post wydaje się zaprzeczać twojemu komentarzowi do postu BalusC - w komentarzach mówisz, że można wykonywać obliczenia w locie, aw swoim poście mówisz, że jest głupi. Czy mogę zapytać, gdzie wyznaczasz linię?
Sevas
To ruchoma skala, a nie kwestia czarno-biała. Niektóre rzeczy najwyraźniej nie stanowią problemu, np. Dodanie kilku wartości, ponieważ zajmują mniej niż milionową część sekundy (w rzeczywistości znacznie mniej). Niektóre najwyraźniej stanowią problem, na przykład dostęp do bazy danych lub plików, ponieważ mogą zająć 10 ms lub dłużej - i na pewno musisz je znać, aby w miarę możliwości uniknąć ich, nie tylko w programach pobierających. Ale dla wszystkiego innego, linia jest tam, gdzie mówi profiler.
Michael Borgwardt,
-1

Radziłbym również przy użyciu takich ram jak Primefaces zamiast zapasów JSF, rozwiązują one takie problemy przed zespołem e JSF. gw podstawach można ustawić częściowe przesłanie. W przeciwnym razie BalusC dobrze to wyjaśnił.

Martin Karari
źródło
-2

To wciąż duży problem w JSF. Na przykład, jeśli masz metodę isPermittedToBlaBlakontroli bezpieczeństwa i według niej masz, rendered="#{bean.isPermittedToBlaBla}metoda ta będzie wywoływana wiele razy.

Kontrola bezpieczeństwa może być skomplikowana np. Zapytanie LDAP itp. Musisz tego uniknąć za pomocą

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

i musisz upewnić się, że w ramach sesji fasola to na żądanie.

Ich zdaniem JSF musi zaimplementować tutaj niektóre rozszerzenia, aby uniknąć wielu połączeń (np. Adnotacja wywołuje @Phase(RENDER_RESPONSE)tę metodę tylko raz po RENDER_RESPONSEfazie ...)

Morad
źródło
2
Możesz zapisać wynik w pamięci podręcznej w RequestParameterMap
Christophe Roussy