JSF: Não coloque lógica cara em métodos getters
Método getter invocado múltiplas vezes? Páginas lentas? Conheça o 2o mau hábito dos desenvolvedores JSF e aprenda a evitá-lo
Você já percebeu que algumas páginas JSF são mais lentas que outras para abrir? Mesmo as mais simples carregam lentamente, por quê?
Um dos maiores problemas de desempenho no carregamento de páginas JSF está intimamente relacionado ao que você coloca dentro dos métodos getters dos managed beans. Uma página pode demorar até 10x mais para abrir no navegador caso você não tome alguns cuidados e adote boas práticas. O que estou querendo dizer é que uma simples consulta ao banco de dados colocada no método errado do seu managed bean pode tornar a experiência dos seus usuários um verdadeiro tormento.
Imagine uma página onde você precisa exibir uma tabela com alguns registros do banco de dados pré-carregados. Algo como na imagem:
Nesse caso, usamos o componente h:dataTable
:
<h:dataTable value="#{clienteBean.clientes}" var="c">
<!-- colunas da tabela -->
</h:dataTable>
O managed bean teria um código semelhante a este:
@Named
public class ClienteBean {
@Inject
private ClienteDao dao;
public List<Cliente> getClientes() {
System.out.println("carregando clientes...");
return dao.listaTudo(); // carregando lista do banco
}
}
Agora, ao acessar a página tudo funcionará como esperado, mas se você habilitar os logs do Hibernate e olhar o console da sua IDE você verá algo desse tipo:
carregando clientes...
Hibernate: select p0_.* from Cliente p0_
carregando clientes...
Hibernate: select p0_.* from Cliente p0_
carregando clientes...
Hibernate: select p0_.* from Cliente p0_
Não demora para perceber que o componente h:dataTable
da página está invocando 3x o método getClientes()
e este, por sua vez, executa 3x a mesma consulta. São 2 consultas desnecessárias ao banco de dados!
Em desenvolvimento é difícil perceber o impacto, pois tudo roda local, existem pouquissímos dados no banco de dados e temos somente 1 usuário (você, claro) usando a aplicação. Mas em produção a coisa toda muda de figura!
E aí, já consegue imaginar o problema de desempenho da sua página? Não pára por aí, pode ser ainda pior...
Repare que simplesmente abrimos a página no navegador; se você submeter o formulário, clicando no botão pesquisar, o número de consultas sobe de 3 para 12:
carregando clientes...
Hibernate: select p0_.* from Produto p0_
carregando clientes...
Hibernate: select p0_.* from Produto p0_
...
carregando clientes...
Hibernate: select p0_.* from Produto p0_
carregando clientes...
Hibernate: select p0_.* from Produto p0_
Este problema é tão sério e atinge tantas aplicações, que em 2008 ela entrou para lista dos 10 maus hábitos dos desenvolvedores JSF. Se você não conhece estes maus hábitos então vale a pena a leitura dos slides.
Como sempre, mais importante do que resolver o sintoma do problema é entender porquê ele acontece...
Como as ELs são processadas
Antes de mais nada, uma EL (Expression Language) é qualquer expressão na página entre #{ }
, elas são utilizadas para fazer o binding entre os componentes visuais e os managed beans. Alguns exemplos são: #{1+1}
ou #{bean.propriedade}
ou mesmo #{bean.metodo()}
.
É muito comum pensar que os componentes armazenam os dados (objetos) dos managed beans retornados pelas EL's, mas não é bem isso o que acontece. Se o componente fizesse isso, a memória do servidor seria consumida rapidamente:
<h:dataTable value="#{clienteBean.clientes}">
<!-- ... -->
</h:dataTable>
O que de fato o componente faz é armazenar o texto da EL, a String
em si, neste caso o texto "#{clienteBean.clientes}". Dessa forma, sempre que o componente precisa dos dados do managed bean, ele avalia a EL, ou seja, ele invoca o método getter do managed bean, faz o que tem que fazer com o objeto retornado pela EL e, por fim, descarta-o.
O problema é que, numa requisição web, um componente pode avaliar uma EL uma ou mais vezes se achar necessário. No nosso caso, a EL #{clienteBean.clientes}
está sendo avaliada várias vezes dentro da mesma requisição, que por sua vez invoca o getter e este dispara a lógica que faz consultas no banco de dados.
Este comportamento é perfeitamente esperado devido a natureza das ELs, a própria especificação JSF comenta sobre essa possibilidade. O número de vezes que um método getter é chamado não depende somente do número de vezes que uma EL é declarada (aparece) na página, mas também depende de vários fatores, entre os principais:
- o ciclo de vida, que pode invocá-lo quantas vezes for necessário;
- da implementação JSF que está em uso;
- do componente que tem a referência a esta EL;
- do estado do componente ou de seu componente pai;
Por exemplo, um h:inputText
pode ter sua EL avaliada 2 ou mais vezes durante o ciclo de vida (conversão, validação, renderização etc), enquanto um componente mais complexo como h:dataTable
pode avaliar mais do que isso.
Falando em componentes de iteração, como h:dataTable
ou ui:repeat
, eles são os campeões nesse aspecto. Além da sua própria EL, todas as ELs dos seus componentes filhos são avaliadas inúmeras vezes dependendo da quantidade de linhas e colunas. O que estou querendo dizer é que uma EL em um h:column
pode ser invocada o dobro ou triplo de vezes, pior ainda se colocada em um atributo como rendered
.
Parece estranho o JSF não cachear os valores dos getters, porém ele não pode assumir que o retorno de um getter será sempre o mesmo, pois o valor pode ser calculado dinamicamente com base noutras variáveis.
Não coloque lógica cara em getters
Avaliar uma EL e invocar seu método getter é uma operação muito barata e você não deveria se preocupar com isso. Contudo, a coisa muda de figura quando você executa alguma lógica cara dentro de um getter, pois como vimos, ele pode ser invocado múltiplas vezes.
Por esse motivo, você deve adotar a boa prática: não coloque lógica cara ou custosa dentro de métodos getters dos managed beans.
Quando falo de lógica cara eu me refiro a consultas a banco de dados ou Web Services; acesso a disco ou rede; ou mesmo algoritimos e calculos demorados. Mas se não posso colocar lógica dentro de getters, onde mais posso colocar?
A partir de agora seu getter deve retornar somente valores prontos ou pré-processados e, para isso, você deve optar por alternativas que são executadas somente uma vez na mesma requisição - muitas delas você já deve conhecer:
callbacks de inicialização como
@PostConstruct
:@PostConstruct public void init() { this.clientes = dao.listaTudo(); // processo custoso }
Prefira usar
@PostConstruct
a blocos de inicialização ou construtores, devido as magias negras de frameworks como CDI ou Spring.ou listeners de ações e eventos do usuário, como uma action ou actionListener:
<h:commandButton value="Gravar" action="#{bean.gravar}" />
ou listeners AJAX como
<f:ajax listener />
:void ajaxListener(AjaxBehaviorEvent event) { // lógica cara }
ou eventos de carregamento de páginas, como o evento preRenderView ou
f:viewAction
:<!-- antes do jsf 2.2 --> <f:metadata> <f:event type="preRenderView" listener="#{bean.init}" /> </f:metadata> <!-- com jsf 2.2 --> <f:metadata> <f:viewAction action="#{bean.init}" /> </f:metadata>
E se eu não posso usar essas alternativas?
Se por algum motivo você não puder utilizar alguma das alternativas acima e tiver que manter a lógica custosa dentro do getter então o melhor que você pode fazer é garantir que a lógica será executada somente uma vez:
public List<Cliente> getClientes() {
if (this.clientes == null) {
this.clientes = dao.listaTudo(); // processo custoso
}
return this.clientes;
}
Basicamente introduzimos o conceito de lazy loading dentro do getter: se a propriedade é null
então carregue-a e associe o resultado a propriedade, caso contrário retorne-a. Dessa forma, garantimos que a lógica não será executada a cada chamada do getter.
Se faz para um faz para todos
Se você faz isso para 1 componente as chances são que você faz isso para todos. Portanto, imagine uma página repleta de componentes referenciando ELs com lógicas caras? Xii... melhor revisar suas páginas!
Você percebe a gravidade dessa má prática? Não é à toa que ela é considerada o 2o mau hábito nas aplicações JSF: ela impacta diretamente na qualidade externa do sistema e, o pior, só é percebida em produção na cara do cliente.
Não há como saber quantas vezes um componente avaliará uma EL dentro do ciclo de vida. Por esse motivo, é recomendado que se evite colocar processamento caro dentro dos métodos getters, principalmente em métodos utilizados em componentes de iteração.
Apesar da solução do lazy loading ser a mais simples de aplicar, você deveria evitá-la e, se possível, orientar os novos desenvolvedores a buscar alternativas melhores. O bom e velho @PostConstruct
resolve boa parte dos cenários.
Enfim, os 10 maus hábitos são discutidos de forma simples e didática no nosso curso JSF 2, PrimeFaces e Spring, onde o aluno cai no problema, percebe seus danos ao sistema e o resolve de maneira apropriada.
E aí, que abordagem você usa para evitar que seus getters (lógicas caras) sejam chamados múltiplas vezes?
You might also be interested in these articles...
Desenvolvedor e instrutor na TriadWorks