Por que o JSF chama getters várias vezes


256

Digamos que eu especifique um componente outputText como este:

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

Se eu imprimir uma mensagem de log quando o getter for somePropertyfor chamado e carregar a página, será trivial notar que o getter está sendo chamado mais de uma vez por solicitação (duas ou três vezes foi o que aconteceu no meu 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 o valor de somePropertyfor caro de calcular, isso pode ser um problema.

Pesquisei um pouco no Google e percebi que esse é um problema conhecido. Uma solução alternativa era incluir uma verificação e verificar se ela já havia sido calculada:

private String someProperty;

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

O principal problema disso é que você obtém um monte de código padrão, sem mencionar variáveis ​​particulares que podem não ser necessárias.

Quais são as alternativas para essa abordagem? Existe uma maneira de conseguir isso sem tanto código desnecessário? Existe uma maneira de impedir que o JSF se comporte dessa maneira?

Obrigado pela sua contribuição!

Respostas:


340

Isso é causado pela natureza das expressões adiadas #{}(observe que expressões padrão "legadas" ${}se comportam exatamente da mesma maneira quando Facelets é usado em vez de JSP). A expressão adiada não é avaliada imediatamente , mas criada como um ValueExpressionobjeto e o método getter por trás da expressão é executado sempre que o código chama ValueExpression#getValue().

Isso normalmente será chamado uma ou duas vezes por ciclo de solicitação-resposta JSF, dependendo se o componente é um componente de entrada ou saída ( saiba aqui ). No entanto, essa contagem pode subir (muito) mais alto quando usada na iteração de componentes JSF (como <h:dataTable>e <ui:repeat>), ou aqui e ali em uma expressão booleana como o renderedatributo O JSF (especificamente, EL) não armazenará em cache o resultado avaliado da expressão EL, pois pode retornar valores diferentes em cada chamada (por exemplo, quando depende da linha de dados iterada no momento).

Avaliar uma expressão EL e chamar um método getter é uma operação muito barata, portanto você geralmente não deve se preocupar com isso. No entanto, a história muda quando você está executando uma lógica de banco de dados / negócios cara no método getter, por algum motivo. Isso seria reexecutado toda vez!

Os métodos Getter nos beans de backup JSF devem ser projetados para que retornem apenas a propriedade já preparada e nada mais, exatamente conforme a especificação Javabeans . Eles não devem executar nenhuma lógica cara de negócios / banco de dados. Para isso @PostConstruct, devem ser utilizados os métodos do ouvinte do bean e / ou (ação). Eles são executados apenas uma vez em algum momento do ciclo de vida JSF baseado em solicitação e é exatamente isso que você deseja.

Aqui está um resumo de todas as maneiras corretas de predefinir / carregar uma propriedade.

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

}

Observe que você não deve usar o construtor ou o bloco de inicialização do bean para o trabalho, pois ele pode ser chamado várias vezes se você estiver usando uma estrutura de gerenciamento de bean que usa proxies, como CDI.

Se você realmente não tem outras maneiras, devido a alguns requisitos restritivos de design, introduza um carregamento lento dentro do método getter. Ou seja, se a propriedade estiver null, carregue e atribua-a à propriedade, caso contrário, devolva-a.

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

        return someProperty;
    }

Dessa forma, a dispendiosa lógica de negócios / banco de dados não será desnecessariamente executada em todas as chamadas getter.

Veja também:


5
Só não use getters para fazer lógica de negócios. Isso é tudo. Reorganize sua lógica de código. Minha aposta é que já foi corrigido usando apenas o método construtor, pós-construção ou ação da maneira mais inteligente.
BalusC

3
-1, discordo totalmente. O objetivo da especificação javaBeans é permitir que as propriedades sejam mais do que apenas um valor de campo, e as "propriedades derivadas" calculadas dinamicamente são perfeitamente normais. Preocupar-se com chamadas getter redundantes não passa de otimização prematura.
Michael Borgwardt

3
Esperar se eles fazem mais do que retornar dados como você tão claramente se :)
BalusC

4
você poderia acrescentar que a inicialização lenta em getters ainda é válido em JSF :)
Bozho

2
@ Harry: Não vai mudar o comportamento. No entanto, você pode manipular qualquer lógica de negócios no getter condicionalmente carregando lentamente e / ou verificando o ID da fase atual por FacesContext#getCurrentPhaseId().
BalusC

17

Com o JSF 2.0, você pode anexar um ouvinte a um evento do sistema

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

Como alternativa, você pode colocar a página JSF em uma f:viewtag

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

      .. jsf page here...

<f:view>

9

Eu escrevi um artigo sobre como armazenar em cache o getter de beans JSF com o Spring AOP.

Eu crio um simples MethodInterceptorque intercepta todos os métodos anotados com uma anotação especial:

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

}

Este interceptor é usado em um arquivo de configuração de mola:

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

Espero que ajude!


6

Originalmente publicado no fórum PrimeFaces @ http://forum.primefaces.org/viewtopic.php?f=3&t=29546

Recentemente, fiquei obcecado em avaliar o desempenho do meu aplicativo, ajustando as consultas JPA, substituindo as consultas dinâmicas SQL por consultas nomeadas e, nesta manhã, reconheci que um método getter era mais um HOT SPOT no Java Visual VM do que o resto de meu código (ou a maioria do meu código).

Método Getter:

PageNavigationController.getGmapsAutoComplete()

Referenciado pela interface do usuário: include in in index.xhtml

Abaixo, você verá que PageNavigationController.getGmapsAutoComplete () é um HOT SPOT (problema de desempenho) no Java Visual VM. Se você olhar mais para baixo, na captura de tela, verá que o método getLazyModel (), o PrimeFaces GetFaz Datatable lento, também é um ponto de acesso, apenas quando o usuário final faz um monte de coisas / operações / tarefas do tipo 'Data Preguiçosa' no aplicativo. :)

Java Visual VM: mostrando o HOT SPOT

Veja o código (original) abaixo.

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

Referenciado pelo seguinte em index.xhtml:

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

Solução: como esse é um método 'getter', mova o código e atribua valor a gmapsAutoComplete antes de o método ser chamado; veja o código abaixo.

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

Resultados do teste: PageNavigationController.getGmapsAutoComplete () não é mais um HOT SPOT no Java Visual VM (nem aparece mais)

Compartilhando este tópico, uma vez que muitos usuários experientes aconselharam os desenvolvedores juniores do JSF a NÃO adicionar código nos métodos 'getter'. :)


4

Se você estiver usando CDI, poderá usar os métodos Producers. Ele será chamado muitas vezes, mas o resultado da primeira chamada é armazenado em cache no escopo do bean e é eficiente para getters que estão computando ou inicializando objetos pesados! Veja aqui , para mais informações.


3

Você provavelmente poderia usar o AOP para criar algum tipo de Aspect que armazenou em cache os resultados de nossos getters por um período configurável. Isso impediria a necessidade de copiar e colar o código padrão em dezenas de acessadores.


É sobre este AOP da primavera que você está falando? Você saberia onde eu poderia encontrar um trecho de código ou dois lidando com aspectos? Ler todo o capítulo 6 da documentação Primavera parece um exagero como eu não estou usando Primavera;)
Sevas

-1

Se o valor de someProperty for caro de calcular, isso pode ser um problema.

Isso é o que chamamos de otimização prematura. Nos raros casos em que um criador de perfil diz que o cálculo de uma propriedade é tão extraordinariamente caro que chamá-la três vezes em vez de uma vez tem um impacto significativo no desempenho, você adiciona o cache conforme descreve. Mas, a menos que você faça algo realmente estúpido, como fatorar primos ou acessar um banco de dados em um getter, seu código provavelmente terá uma dúzia de ineficiências piores em lugares em que você nunca pensou.


Daí a pergunta - se someProperty corresponde a algo caro para calcular (ou como você acessa um banco de dados ou fatores primos de fatoração), qual é a melhor maneira de evitar fazer o cálculo várias vezes por solicitação e é a solução listada na pergunta o melhor. Se você não está respondendo à pergunta, os comentários são um bom lugar para postar, não? Além disso, seu post parece contradizer seu comentário no post de BalusC - nos comentários que você diz que é bom fazer cálculos em tempo real, e no seu post você diz que é estúpido. Posso perguntar onde você desenha a linha?
Sevas

É uma escala móvel, não um problema em preto e branco. Algumas coisas claramente não são um problema, por exemplo, adicionar alguns valores, porque levam menos de um milionésimo de segundo ( muito menos, na verdade). Alguns claramente são um problema, como acesso ao banco de dados ou a arquivos, porque podem levar 10 ms ou mais - e você definitivamente precisa conhecê-los para evitá-los, se possível, não apenas em getters. Mas para todo o resto, a linha é onde o criador de perfis diz a você.
Michael Borgwardt

-1

Eu também recomendaria usar o Framework como Primefaces em vez do JSF padrão, eles abordam esses problemas antes da equipe do JSF e. g nos primefaces, você pode definir o envio parcial. Caso contrário, o BalusC explicou bem.


-2

Ainda é um grande problema no JSF. Por exemplo, se você possui um método isPermittedToBlaBlapara verificações de segurança e, na sua opinião, possui rendered="#{bean.isPermittedToBlaBla}, o método será chamado várias vezes.

A verificação de segurança pode ser complicada, por exemplo. Consulta LDAP etc. Portanto, você deve evitar isso com

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

e você deve garantir isso dentro de um bean de sessão por solicitação.

Acho que o JSF deve implementar aqui algumas extensões para evitar várias chamadas (por exemplo, anotação chamada @Phase(RENDER_RESPONSE)esse método apenas uma vez após a RENDER_RESPONSEfase ...)


2
Você pode armazenar em cache o resultado na RequestParameterMap
Christophe Roussy
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.