Pourquoi JSF appelle plusieurs fois les getters

256

Disons que je spécifie un composant outputText comme ceci:

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

Si j'imprime un message de journal lorsque le getter pour somePropertyest appelé et charge la page, il est trivial de remarquer que le getter est appelé plus d'une fois par demande (deux ou trois fois c'est ce qui s'est passé dans mon cas):

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

Si la valeur de somePropertyest coûteuse à calculer, cela peut potentiellement être un problème.

J'ai googlé un peu et j'ai pensé que c'était un problème connu. Une solution de contournement consistait à inclure un chèque et à voir s'il avait déjà été calculé:

private String someProperty;

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

Le principal problème avec cela est que vous obtenez des charges de code passe-partout, sans parler des variables privées dont vous pourriez ne pas avoir besoin.

Quelles sont les alternatives à cette approche? Existe-t-il un moyen d'y parvenir sans autant de code inutile? Existe-t-il un moyen d'empêcher JSF de se comporter de cette manière?

Merci pour votre contribution!

Sevas
la source

Réponses:

340

Cela est dû à la nature des expressions différées #{}(notez que les expressions standard "héritées" ${}se comportent exactement de la même manière lorsque Facelets est utilisé à la place de JSP). L'expression différée n'est pas immédiatement évaluée, mais créée en tant ValueExpressionqu'objet et la méthode getter derrière l'expression est exécutée à chaque fois que le code appelle ValueExpression#getValue().

Cela sera normalement invoqué une ou deux fois par cycle de demande-réponse JSF, selon que le composant est un composant d'entrée ou de sortie ( découvrez-le ici ). Cependant, ce nombre peut augmenter (beaucoup) plus haut lorsqu'il est utilisé dans l'itération de composants JSF (tels que <h:dataTable>et <ui:repeat>), ou ici et là dans une expression booléenne comme l' renderedattribut. JSF (en particulier, EL) ne mettra pas du tout en cache le résultat évalué de l'expression EL car il peut renvoyer des valeurs différentes à chaque appel (par exemple, lorsqu'il dépend de la ligne datable actuellement itérée).

Évaluer une expression EL et invoquer une méthode getter est une opération très bon marché, donc vous ne devriez généralement pas vous en soucier du tout. Cependant, l'histoire change lorsque vous effectuez une logique DB / métier coûteuse dans la méthode getter pour une raison quelconque. Ce serait à chaque fois réexécuté!

Les meilleures méthodes dans les beans de sauvegarde JSF doivent être conçues de manière à renvoyer uniquement la propriété déjà préparée et rien de plus, exactement selon la spécification Javabeans . Ils ne devraient pas du tout faire de logique DB / métier coûteuse. Pour cela, les @PostConstructméthodes d'écoute du bean et / ou (action) doivent être utilisées. Ils ne sont exécutés qu'une seule fois à un moment donné du cycle de vie JSF basé sur les demandes et c'est exactement ce que vous voulez.

Voici un résumé de toutes les bonnes façons de prérégler / charger une propriété.

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

}

Notez que vous ne devez pas utiliser le constructeur ou le bloc d'initialisation du bean pour le travail car il peut être invoqué plusieurs fois si vous utilisez un framework de gestion de bean qui utilise des proxys, tels que CDI.

S'il n'y a vraiment pas d'autre moyen pour vous, en raison de certaines exigences de conception restrictives, vous devez introduire le chargement paresseux dans la méthode getter. C'est-à-dire si la propriété est null, puis chargez-la et affectez-la à la propriété, sinon renvoyez-la.

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

        return someProperty;
    }

De cette façon, la logique DB / métier coûteuse ne sera pas inutilement exécutée à chaque appel getter unique.

Voir également:

BalusC
la source
5
N'utilisez simplement pas de getters pour faire de la logique métier. C'est tout. Réorganisez votre logique de code. Je parie qu'il est déjà corrigé en utilisant simplement le constructeur, la post-construction ou la méthode d'action de manière intelligente.
BalusC
3
-1, fortement en désaccord. L'intérêt de la spécification javaBeans est de permettre aux propriétés d'être plus qu'une simple valeur de champ, et les "propriétés dérivées" qui sont calculées à la volée sont parfaitement normales. S'inquiéter des appels getter redondants n'est rien d'autre qu'une optimisation prématurée.
Michael Borgwardt
3
Attendez-vous à ce qu'ils fassent plus que renvoyer des données comme vous l'avez si clairement déclaré :)
BalusC
4
vous pouvez ajouter que l'initialisation paresseuse dans les getters est toujours valide dans JSF :)
Bozho
2
@Harry: Cela ne changera pas le comportement. Vous pouvez cependant gérer conditionnellement n'importe quelle logique métier dans le getter par chargement paresseux et / ou en vérifiant l'ID de phase en cours par FacesContext#getCurrentPhaseId().
BalusC
17

Avec JSF 2.0, vous pouvez attacher un écouteur à un événement système

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

Vous pouvez également inclure la page JSF dans une f:viewbalise

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

      .. jsf page here...

<f:view>
César Alforde
la source
9

J'ai écrit un article sur la façon de mettre en cache le getter de beans JSF avec Spring AOP.

Je crée un simple MethodInterceptorqui intercepte toutes les méthodes annotées avec une annotation spéciale:

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

}

Cet intercepteur est utilisé dans un fichier de configuration de ressort:

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

J'espère que cela vous aidera!

Nicolas Labrot
la source
6

Publié à l'origine sur le forum PrimeFaces @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

Récemment, j'ai été obsédé par l'évaluation des performances de mon application, le réglage des requêtes JPA, le remplacement des requêtes SQL dynamiques par des requêtes nommées, et ce matin, j'ai reconnu qu'une méthode getter était plus un HOT SPOT dans Java Visual VM que le reste de mon code (ou la majorité de mon code).

Méthode Getter:

PageNavigationController.getGmapsAutoComplete()

Référencé par ui: include in dans index.xhtml

Ci-dessous, vous verrez que PageNavigationController.getGmapsAutoComplete () est un HOT SPOT (problème de performance) dans Java Visual VM. Si vous regardez plus bas, sur la capture d'écran, vous verrez que getLazyModel (), la méthode getter dateable lazy PrimeFaces, est également un point chaud, uniquement lorsque l'utilisateur final fait beaucoup de choses 'opérations paresseuses'. dans l'application. :)

Java Visual VM: affichage de HOT SPOT

Voir le code (original) ci-dessous.

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

Référencé par ce qui suit dans index.xhtml:

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

Solution: puisqu'il s'agit d'une méthode «getter», déplacez le code et attribuez une valeur à gmapsAutoComplete avant d'appeler la méthode; voir le code ci-dessous.

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

Résultats des tests: PageNavigationController.getGmapsAutoComplete () n'est plus un HOT SPOT dans Java Visual VM (n'apparaît même plus)

Partager ce sujet, car de nombreux utilisateurs experts ont conseillé aux développeurs JSF juniors de ne PAS ajouter de code dans les méthodes «getter». :)

Howard
la source
4

Si vous utilisez CDI, vous pouvez utiliser les méthodes Producers. Il sera appelé plusieurs fois, mais le résultat du premier appel est mis en cache dans la portée du bean et est efficace pour les getters qui calculent ou initialisent des objets lourds! Voir ici , pour plus d'informations.

Heidarzadeh
la source
3

Vous pouvez probablement utiliser AOP pour créer une sorte d'aspect qui met en cache les résultats de nos getters pendant une durée configurable. Cela vous éviterait d'avoir à copier et coller du code passe-partout dans des dizaines d'accesseurs.

mat b
la source
Est-ce ce AOP de printemps dont vous parlez? Savez-vous où je pourrais trouver un extrait de code ou deux traitant des aspects? La lecture de l'intégralité du 6ème chapitre de la documentation de Spring semble exagérée car je n'utilise pas Spring;)
Sevas
-1

Si la valeur de someProperty est coûteuse à calculer, cela peut potentiellement être un problème.

C'est ce que nous appelons une optimisation prématurée. Dans les rares cas où un profileur vous indique que le calcul d'une propriété est si extraordinairement coûteux que son appel trois fois plutôt qu'une fois a un impact significatif sur les performances, vous ajoutez la mise en cache comme vous le décrivez. Mais à moins que vous ne fassiez quelque chose de vraiment stupide comme factoriser des nombres premiers ou accéder à une base de données dans un getter, votre code a probablement une douzaine d'inefficacités pires dans des endroits auxquels vous n'avez jamais pensé.

Michael Borgwardt
la source
D'où la question - si une propriété correspond à quelque chose de cher à calculer (ou, comme vous le dites, accéder à une base de données ou à des facteurs d'affacturage), quelle est la meilleure façon d'éviter de faire le calcul plusieurs fois par demande et est la solution que j'ai listée dans la question la meilleur. Si vous ne répondez pas à la question, les commentaires sont un bon endroit pour publier, non? De plus, votre message semble contredire votre commentaire sur le message de BalusC - dans les commentaires, vous dites que c'est bien de faire des calculs à la volée, et dans votre message, vous dites que c'est stupide. Puis-je vous demander où vous tracez la ligne?
Sevas
C'est une échelle mobile, pas un problème en noir et blanc. Certaines choses ne sont clairement pas un problème, par exemple l'ajout de quelques valeurs, car elles prennent moins d'un millionième de seconde ( beaucoup moins, en fait). Certains sont clairement un problème, comme l'accès aux bases de données ou aux fichiers, car ils peuvent prendre 10 ms ou plus - et vous devez absolument les connaître afin de les éviter si possible, pas seulement dans les getters. Mais pour tout le reste, la ligne est l'endroit où le profileur vous le dit.
Michael Borgwardt
-1

Je conseillerais également d'utiliser un tel cadre comme Primefaces au lieu du stock JSF, ils traitent ces problèmes avant l'équipe JSF e. g dans les primefaces, vous pouvez définir une soumission partielle. Sinon, BalusC l'a bien expliqué.

Martin Karari
la source
-2

C'est toujours un gros problème en JSF. Par exemple, si vous avez une méthode isPermittedToBlaBlapour les contrôles de sécurité et que selon vous, vous avez rendered="#{bean.isPermittedToBlaBla}alors la méthode sera appelée plusieurs fois.

Le contrôle de sécurité pourrait être compliqué, par exemple. Requête LDAP etc. Vous devez donc éviter cela avec

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

et vous devez vous assurer au sein d'une session bean ceci par requête.

Je pense que JSF doit implémenter ici quelques extensions pour éviter les appels multiples (ex: annotation ne @Phase(RENDER_RESPONSE)calle cette méthode qu'une seule fois après la RENDER_RESPONSEphase ...)

Morad
la source
2
Vous pouvez mettre en cache le résultat dans le RequestParameterMap
Christophe Roussy