Perché JSF chiama getter più volte


256

Diciamo che specifica un componente outputText come questo:

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

Se stampo un messaggio di registro quando somePropertyviene chiamato il getter per e carico la pagina, è banale notare che il getter viene chiamato più di una volta per richiesta (due o tre volte è quello che è successo nel mio caso):

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

Se il valore di somePropertyè costoso da calcolare, questo può potenzialmente essere un problema.

Ho cercato su Google un po 'e ho pensato che questo fosse un problema noto. Una soluzione alternativa consisteva nel includere un controllo e vedere se era già stato calcolato:

private String someProperty;

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

Il problema principale è che ottieni un sacco di codice boilerplate, per non parlare delle variabili private che potresti non aver bisogno.

Quali sono le alternative a questo approccio? C'è un modo per raggiungere questo obiettivo senza tanto codice inutile? C'è un modo per impedire a JSF di comportarsi in questo modo?

Grazie per il tuo contributo!

Risposte:


340

Ciò è causato dalla natura delle espressioni differite #{}(si noti che le espressioni standard "legacy" si ${}comportano esattamente allo stesso modo quando si utilizza Facelets anziché JSP). L'espressione differita non viene immediatamente valutata, ma creata come ValueExpressionoggetto e il metodo getter dietro l'espressione viene eseguito ogni volta che il codice chiama ValueExpression#getValue().

Questo sarà normalmente invocato una o due volte per ciclo di richiesta-risposta JSF, a seconda che il componente sia un componente di input o output ( apprendilo qui ). Tuttavia, questo conteggio può aumentare (molto) più in alto se usato in iterazione di componenti JSF (come <h:dataTable>e <ui:repeat>), oppure qua e là in un'espressione booleana come l' renderedattributo. JSF (in particolare, EL) non memorizzerà nella cache il risultato valutato dell'espressione EL in quanto potrebbe restituire valori diversi su ogni chiamata (ad esempio, quando dipende dalla riga databile attualmente ripetuta).

Valutare un'espressione EL e invocare un metodo getter è un'operazione molto economica, quindi generalmente non dovresti preoccuparti di questo. Tuttavia, la storia cambia quando si eseguono costose DB / business logic nel metodo getter per qualche motivo. Questo sarebbe ri-eseguito ogni volta!

I metodi getter nei bean di supporto JSF dovrebbero essere progettati in modo tale da restituire esclusivamente la proprietà già preparata e niente di più, esattamente come da specifica Javabeans . Non dovrebbero assolutamente fare costose DB / business logic. A tale scopo @PostConstruct, è necessario utilizzare i metodi di ascolto del bean e / o (azione). Vengono eseguiti solo una volta in un punto del ciclo di vita JSF basato su richiesta ed è esattamente quello che vuoi.

Ecco un riepilogo di tutti i diversi modi giusti per preimpostare / caricare una proprietà.

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

}

Si noti che è necessario non utilizzare il costruttore o l'inizializzazione del blocco del bean per il lavoro, perché può essere invocato più volte se si sta utilizzando un quadro di gestione di fagioli che usa le procure, come CDI.

Se non ci sono altri modi per te, a causa di alcuni requisiti di progettazione restrittivi, allora dovresti introdurre un caricamento lento all'interno del metodo getter. Vale a dire se la proprietà è null, quindi caricarla e assegnarla alla proprietà, altrimenti restituirla.

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

        return someProperty;
    }

In questo modo la costosa DB / business logic non verrà inutilmente eseguita ad ogni singola chiamata getter.

Guarda anche:


5
Basta non usare getter per fare business logic. È tutto. Riorganizza la tua logica del codice. Scommetto che è già stato risolto semplicemente usando il metodo di costruzione, postcostruzione o azione in modo intelligente.
BalusC,

3
-1, in forte disaccordo. L'intero punto delle specifiche javaBeans è consentire alle proprietà di essere più di un semplice valore di campo e le "proprietà derivate" che vengono calcolate al volo sono perfettamente normali. Preoccuparsi di chiamate getter ridondanti non è altro che ottimizzazione prematura.
Michael Borgwardt,

3
Aspettati di fare qualcosa in più rispetto alla restituzione dei dati, come hai affermato chiaramente te stesso :)
BalusC

4
potresti aggiungere che l'inizializzazione
lenta in Getter

2
@Harry: non cambierà il comportamento. Tuttavia, è possibile gestire in modo condizionale qualsiasi logica aziendale nel getter caricando in modo lento e / o controllando l'ID fase corrente mediante FacesContext#getCurrentPhaseId().
BalusC

17

Con JSF 2.0 è possibile collegare un ascoltatore a un evento di sistema

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

In alternativa è possibile racchiudere la pagina JSF in un f:viewtag

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

      .. jsf page here...

<f:view>

9

Ho scritto un articolo su come memorizzare nella cache i bean JSF getter con Spring AOP.

Creo un semplice MethodInterceptorche intercetta tutti i metodi annotati con un'annotazione speciale:

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

}

Questo intercettore viene utilizzato in un file di configurazione di primavera:

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

Spero che possa aiutare!


6

Originariamente pubblicato nel forum PrimeFaces @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

Di recente, sono stato ossessionato dalla valutazione delle prestazioni della mia app, dall'ottimizzazione delle query JPA, dalla sostituzione delle query SQL dinamiche con query denominate e proprio stamattina ho riconosciuto che un metodo getter era più uno SPOT CALDO in Java Visual VM che il resto di il mio codice (o la maggior parte del mio codice).

Metodo Getter:

PageNavigationController.getGmapsAutoComplete()

Riferito a ui: include in in index.xhtml

Di seguito, vedrai che PageNavigationController.getGmapsAutoComplete () è un HOT SPOT (problema di prestazioni) in Java Visual VM. Se guardi più in basso, sullo schermo, vedrai che getLazyModel (), il metodo getter datatable pigro di PrimeFaces, è anche un hot spot, solo quando l'utilizzatore sta facendo un sacco di cose / operazioni / attività "databili pigri" nell'app. :)

Java Visual VM: mostra HOT SPOT

Vedi il codice (originale) di seguito.

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

Riferito da quanto segue in index.xhtml:

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

Soluzione: poiché si tratta di un metodo "getter", spostare il codice e assegnare il valore a gmapsAutoComplete prima di chiamare il metodo; vedi codice sotto.

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

Risultati del test: PageNavigationController.getGmapsAutoComplete () non è più uno HOT SPOT in Java Visual VM (non viene più visualizzato)

Condividendo questo argomento, poiché molti utenti esperti hanno consigliato agli sviluppatori JSF junior di NON aggiungere codice nei metodi "getter". :)


4

Se si utilizza CDI, è possibile utilizzare i metodi Producers. Verrà chiamato molte volte, ma il risultato della prima chiamata viene memorizzato nella cache nell'ambito del bean ed è efficiente per i getter che elaborano o inizializzano oggetti pesanti! Vedi qui , per maggiori informazioni.


3

Probabilmente potresti usare AOP per creare una sorta di Aspetto che memorizza nella cache i risultati dei nostri getter per un periodo di tempo configurabile. Ciò eviterebbe di dover copiare e incollare il codice del boilerplate in dozzine di accessori.


Questa primavera AOP stai parlando? Sapresti dove potrei trovare uno snippet di codice o due relativi ad Aspects? Leggere l'intero sesto capitolo della documentazione di Spring sembra eccessivo dato che non sto usando Spring;)
Sevas,

-1

Se il valore di someProperty è costoso da calcolare, questo può potenzialmente essere un problema.

Questo è ciò che chiamiamo ottimizzazione prematura. Nel raro caso in cui un profiler ti informi che il calcolo di una proprietà è così straordinariamente costoso che chiamarla tre volte anziché una volta ha un impatto significativo sulle prestazioni, aggiungi la cache mentre descrivi. Ma a meno che tu non faccia qualcosa di veramente stupido come il factoring primi o l'accesso a un databse in un getter, il tuo codice molto probabilmente ha una dozzina di inefficienze peggiori in luoghi a cui non hai mai pensato.


Da qui la domanda: se someProperty corrisponde a qualcosa di costoso da calcolare (o quando lo si accede a un database o numeri primi di factoring), qual è il modo migliore per evitare di fare il calcolo più volte per richiesta ed è la soluzione che ho elencato nella domanda il migliore. Se non rispondi alla domanda, i commenti sono un buon posto per pubblicare, no? Inoltre, il tuo post sembra contraddire il tuo commento sul post di BalusC - nei commenti dici che va bene fare calcoli al volo, e nel tuo post dici che è stupido. Posso chiederti dove disegni la linea?
Sevas,

È una scala mobile, non un problema in bianco e nero. Alcune cose non sono chiaramente un problema, ad esempio aggiungendo alcuni valori, perché impiegano meno di un milionesimo di secondo ( molto meno, in realtà). Alcuni chiaramente sono un problema, come l'accesso al DB o ai file, perché possono richiedere 10ms o più - e sicuramente devi conoscerli in modo da poterli evitare, se possibile, non solo nei getter. Ma per tutto il resto, la linea è dove ti dice il profiler.
Michael Borgwardt,

-1

Vorrei anche consigliare di utilizzare Framework come Primefaces anziché JSF di serie, che affrontano tali problemi prima del team JSF e. g in primefaces è possibile impostare l'invio parziale. Altrimenti BalusC lo ha spiegato bene.


-2

È ancora un grosso problema in JSF. Ad esempio, se si dispone di un metodo isPermittedToBlaBlaper i controlli di sicurezza e, a tuo avviso, rendered="#{bean.isPermittedToBlaBla}il metodo verrà chiamato più volte.

Il controllo di sicurezza potrebbe essere complicato, ad es. Query LDAP ecc. Quindi è necessario evitarlo con

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

e devi assicurarti all'interno di un bean di sessione questo per richiesta.

Penso che JSF debba implementare qui alcune estensioni per evitare chiamate multiple (ad es. L'annotazione chiama @Phase(RENDER_RESPONSE)questo metodo solo una volta dopo la RENDER_RESPONSEfase ...)


2
È possibile memorizzare nella cache il risultato in RequestParameterMap
Christophe Roussy il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.