web-dev-qa-db-fra.com

Pourquoi JSF appelle les Getters plusieurs fois

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 de someProperty est appelé et charge la page, il est trivial de remarquer que le getter est appelé plusieurs 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 someProperty est coûteuse à calculer, cela peut potentiellement être un problème.

J'ai un peu cherché sur Google et je me suis dit que c'était un problème connu. Une solution de contournement consistait à inclure un contrôle 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 ceci est que vous obtenez des charges de code passe-partout, sans parler des variables privées dont vous n’auriez peut-être pas besoin.

Quelles sont les alternatives à cette approche? Y a-t-il un moyen d'y parvenir sans autant de code inutile? Y a-t-il un moyen d'empêcher JSF de se comporter de la sorte?

Merci pour votre contribution!

251
Sevas

Cela est dû à la nature des expressions différées #{} (notez que les expressions standard "anciennes" ${} 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 qu'objet ValueExpression et la méthode d'accesseur derrière l'expression est exécutée à chaque fois que le code appelle ValueExpression#getValue() .

Ceci sera normalement invoqué une ou deux fois par cycle requête-réponse JSF, selon que le composant est un composant d'entrée ou de sortie ( apprenez-le ici ). Cependant, ce nombre peut être (beaucoup) plus élevé lorsqu'il est utilisé pour itérer des composants JSF (tels que <h:dataTable> et <ui:repeat>), ou ici et là dans une expression booléenne telle que l'attribut rendered. 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 pour chaque appel (par exemple, lorsqu'il dépend du paramètre en cours). ligne datatable itérée).

Évaluer une expression EL et invoquer une méthode de lecture est une opération très économique, vous ne devriez donc généralement pas vous en préoccuper. Toutefois, l’histoire change lorsque vous exécutez une logique de base de données/métier coûteuse dans la méthode getter pour une raison quelconque. Ce serait ré-exécuté à chaque fois!

Les méthodes d'accès dans les haricots de support JSF doivent être conçues de manière à ne renvoyer que la propriété déjà préparée et rien de plus, exactement comme indiqué par Spécification Javabeans . Ils ne doivent absolument pas utiliser une logique de base de données/métier coûteuse. Pour cela, les méthodes d'écoute @PostConstruct et/ou (d'action) du bean doivent être utilisées. Ils ne sont exécutés qu'une seule fois à un moment donné du cycle de vie JSF basé sur une requête et c'est exactement ce que vous voulez.

Voici un résumé de toutes les différentes méthodes correctes pour prédéfinir/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 devez ne pas utiliser le constructeur ou le bloc d’initialisation du bean pour le travail car il peut être appelé plusieurs fois si vous utilisez un framework de gestion des beans qui utilise les mandataires, tels que CDI.

S'il n'y a vraiment aucune autre solution pour vous, en raison de contraintes de conception contraignantes, vous devriez alors introduire le chargement paresseux dans la méthode getter. C'est à dire. si la propriété est null, 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;
    }

Ainsi, la coûteuse logique de base de données/métier ne sera pas exécutée inutilement lors de chaque appel getter.

Voir également:

333
BalusC

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 balise f:view

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

      .. jsf page here...

<f:view>
16
César Alforde

J'ai écrit un article sur la mise en cache de get Beans JSF avec Spring AOP.

Je crée un simple MethodInterceptor qui 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 printanier:

    <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 ça va aider!

9
Nicolas Labrot

A l'origine posté dans le forum PrimeFaces @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

Récemment, j'étais 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 de lecture était plus une méthode 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

Vous verrez ci-dessous que PageNavigationController.getGmapsAutoComplete () est un HOT SPOT (problème de performances) dans Java Visual VM. Si vous regardez plus bas, sur la capture d'écran, vous verrez que getLazyModel (), la méthode de collecte de données paresseuse paresseuse PrimeFaces, est également un point chaud, uniquement lorsque l'utilisateur final effectue beaucoup de tâches/opérations/tâches de type "lazy datatable". dans l'application :)

Java Visual VM: showing 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 le suivant 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 l'appel de 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 du test: PageNavigationController.getGmapsAutoComplete () n'est plus un HOT SPOT dans Java Visual VM (ne s'affiche même plus)

Partage de ce sujet, car de nombreux utilisateurs experts ont conseillé aux développeurs JSF débutants de NE PAS ajouter de code dans les méthodes "getter". :)

6
Howard

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.

4
Heidarzadeh

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

3
matt b

C'est toujours un gros problème à JSF. Par exemple, si vous avez une méthode isPermittedToBlaBla pour les contrôles de sécurité et que, dans votre vue, vous avez rendered="#{bean.isPermittedToBlaBla}, 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 que dans un bean de session ceci par demande.

Je pense que JSF doit implémenter certaines extensions pour éviter les appels multiples (par exemple, annotation @Phase(RENDER_RESPONSE) n'appelez cette méthode qu'une seule fois après RENDER_RESPONSE phase ...)

0
Morad