JSF – Lidando com o estado da view

JSF é um framework web MVC que foi criado para desenvolver aplicações web de maneira stateful (além de vários outros motivos). São os componentes do JSF que guardam este estado e por causa deles JSF consegue se lembrar, por exemplo, qual converter ou validator é para usar, ou qual era o valor que o usuário digitou na requisição anterior.

O simples formulário abaixo declara um h:inputText e um h:commandButton. Quando o JSF recebe a requisição inicial (HTTP GET) serão instanciados esses componentes que são do tipo UIForm, UIInput (que é associado com um validator DoubleRangeValidator) e um UICommand. Cada componente estende a classe UIComponent e pode ter filhos, assim usando o famoso design pattern Composite, veja o exemplo:

<h:form>
	<h:inputText value="#{formularioBean.valor}" id="valor">
		<f:validateDoubleRange minimum="0" />
	</h:inputText>
	<h:commandButton value="Enviar" action="#{formularioBean.mostra}"/>
</h:form>

O resultado é a árvore de componentes (também é chamado view ou UIViewRoot). Ela fica salva na HttpSession do usuário. Por isso, JSF sabe se lembrar qual valor (setter) deve ser usado para preencher o nosso modelo (é o componente que guarda isso!) e por isso também não é preciso definir na URI qual método deve ser chamado no bean, que é tão comum nos frameworks action-based. No formulário acima essa informação está encapsulado no UICommand (h:commandButton).

Cada vez que o controlador JSF recebe um novo HTTP GET para uma página (request inicial) será criado uma árvore nova. Para distinguir entre as árvores diferentes, JSF renderiza no HTML sempre um campo a mais que representa a identificação da árvore dentro da sessão HTTP:

<input id="javax.faces.ViewState" value="-5331242430046946924:4161480279607048884"  type="hidden" />

javax.faces.ViewState guarda a identificação da árvore no lado do cliente e para cada GET se repete este procedimento (nova árvore, nova identificação). Ou seja, todas as página vão ter uma ou mais árvores de componentes na sessão (mesmo para uma aplicação com poucas páginas, o usuário poderia usar várias abas da mesma página). Para testar isso basta chamar uma página (GET) e verificar o número do campo javax.faces.ViewState (a identificação deve mudar):

1. HTTP GET -> ViewState ID: 3695116712045768933
2. HTTP GET -> ViewState ID:-7330234494192939826
3. HTTP GET -> ViewState ID: 6490700310803488872
(3 requisições para a mesma página geram 3 árvores diferentes na sessão do usuário)

Isso significa que um usuário mal intencionado poderia enviar milhares de GET para uma página JSF para causar a criação de árvores na sessão até a memória HEAP acabar. Para evitar esse tipo de problema, o controlador JSF limita a quantidade de árvores por usuário. Usando a implementação referencial Mojarra o padrão é de 15 árvores no máximo por sessão. Caso o usuário abra, por exemplo, mais do que 15 abas no navegador, o controlador JSF automaticamente vai tirar a árvore menos usada (LRU) da sessão. Para testar isso podemos usar um filtro que imprime o conteúdo da sessão, como mostra a imagem ao lado. Se o usuário usa uma aba com uma ID que o controlador já tirou recebemos a famosa ViewExpiredException:

Através de uma configuração no web.xml, específica para cada implementação JSF, podemos mudar o padrão. Segue a definição para Mojarra:

<context-param>
   <param-name>com.sun.faces.numberOfViewsInSession</param-name>
   <param-value>5</param-value>
</context-param>

O parâmetro é específico para a implementação Mojarra e só funciona na versão 1.2. Mojarra 2.x não respeita o parâmetro apesar da documentação indicar que ela sirva para ambas versões. A implementação MyFaces também possui uma configuração parecida.

Conseguimos então limitar a quantidade de árvores na sessão. Dependendo da quantidade de usuários e complexidade das telas comparado com o tamanho disponível de memória no servidor podemos mesmo assim enfrentar problemas. Por isso a especificação JSF oferece um recurso para não gravar a árvore de componentes na sessão. Mas onde fica o estado então? Toda árvore do componentes é serializada e é devolvida através de um inputHidden na resposta da requisição. Esse não é o comportamento padrão e deve ser configurado no web.xml, mas faz parte da especificação JSF:

  <context-param>
    <param-name>javax.faces.STATE_SAVING_METHOD</param-name>
    <param-value>client</param-value> <!-- server é o padrão -->
  </context-param> 

Nesse caso não fica mais a identifição da árvore no HTML gerado, e sim a árvore serializada:

Em cada requisição trafega a árvore inteira entre navegador e servidor e no início de cada requisição o servidor recria essa tela. Isso aumenta a banda e uso da CPU no servidor, mas diminui o uso da memória. É importante mencionar que apenas o ViewState será serializado (os componentes) e não o estado da aplicação (ManagedBeans). Caso colocarmos algum ManagedBean na sessão, o bean continua não sessão, não fazendo parte da serialização. O filtro mostra esse comportamento e continua imprimindo o FormularioBean:

A documentação da Mojarra indica que normalmente o client-state é a melhor solução, mas isso é um tradeoff entre desempenho e uso de memoria e deve ser avaliado para cada projeto. Mojarra até oferece soluções misturadas, podemos por exemplo serializar o view-state, mas deixar a árvore serializada na sessão.

21 Comentários

  1. Thiago Marinho 19/10/2011 at 12:09 #

    Obrigado pela dica, estava precisando!

    Estou passando por alguns problemas referente ao estado da view!

    No console do Jboss aparece várias vezes sessão expirada (sessao.expirada) na hora que acesso a app e me autentico nela. fazendo alusão a ViewExpiredException, que tem um tratamento que quando essa classe é “chamada” redireciona para /sessaoexpirada.html e renderiza uma mensagem

    Vou dar uma pesquisa a respeito, obrigado pela iniciativa!

  2. Rafael Ponte 19/10/2011 at 13:55 #

    Nico,

    Acredito que eessa afirmação “Toda árvore do componentes é serializada e é devolvida através de um cookie na resposta da requisição.” não está correta.

    Na verdade, assim como você mostrou na imagem, a árvore de componentes é serializada não em um cookie, mas sim no input hidden dentro do HTML gerado.

    Enfim, excelente post, parabéns!

  3. André 19/10/2011 at 14:04 #

    Bacana o artigo, Nico. Tenho uma pergunta: você conhece alguma forma eficiente de se depurar erros no JSF? Pergunto pois estou passando por um problema de NullPointerException e já criei até um PhaseListener para acompanhar o que pode estar acontecendo. Mas só descobri que ele executa com sucesso o afterPhase da RESTORE_VIEW 1 e nunca chega entrar na segunda “phase”. Como será que consigo debugar o JSF para saber ao menos o ponto onde ele está dando erro?

  4. Nico Steppat 19/10/2011 at 14:06 #

    Oi Rafael, vc tem toda razão. já corrigi no artigo. obrigado.

  5. Víctor Hugo 19/10/2011 at 16:26 #

    Nico Steppat. Excelente post! Me mostrou ainda mais o porque devemos sempre nos questionar a respeito do uso dessa tecnologia.

  6. Thalys de Aguiar Gomes 19/10/2011 at 19:02 #

    Exatamente Vitor, eu nunca foi muito fã desta tecnologia por tantos problemas de desempenho…

  7. Raphael 19/10/2011 at 21:32 #

    o moooonsstro @rponte sempre dando seus pitacos!

    excelente artigo nico!

  8. Edson Marques 20/10/2011 at 13:52 #

    Obrigado pelo artigo! Muito bom!

  9. Bob 22/10/2011 at 11:17 #

    Belo post, mas sinceramente não gosto do JSF… várias vezes temos que contornar as limitações do framework, e acaba atrapalhando mais que ajudando… fora o lixo de código que gera… por mais que a versão 2.0 tenha evoluído, prefiro frames action-based

  10. Thiago dos Santos 08/11/2011 at 12:01 #

    Vale lembrar que o padrão das requisições em JSF é o POST. Quando trabalhamos com POST o controlador do JSF recupera a árvore de componentes previamente criada na primeira requisição GET, não inchando a sessão com várias árvores para uma mesma página. De qualquer modo, esse gargalo de guardar as árvores na sessão do usuário entristece qualquer um.

  11. Marcus Vinicius Soliva 09/11/2011 at 11:26 #

    André – Esse erro acontece quando você tenta aplicar os valores da requisição e o jsf começa a dar “get” em seus dependentes do MB, então da uma olha la no seu MB e ver se tem algum objeto que não foi instanciado.

  12. Eduardo 09/11/2011 at 11:28 #

    Olá Nico,

    Em ASP.NET temos um API que permite salvar o ViewState em disco.
    Desta forma não alocamos banda e memória, apenas I/O para gravar a recuperar o estado.

    Há algo similar em JSF? Caso sim, poderia orienta-rme onde posso encontrar esta estratégia, afinal, seria uma solução interessante.

  13. Victor Pinto 09/11/2011 at 15:16 #

    Vale lembrar que colocar o STATE_SAVING_METHOD para client aumenta consideravalmente o tamanho do html gerado, porque como disse o Rafael Ponte, ele coloca um input hidden que costuma ser gigante. Deve ser considerado a performance da aplicação em alguns casos.

  14. Nico Steppat 09/11/2011 at 17:22 #

    @Eduardo

    Pelo que eu sei não existe nenhuma implementação JSF que faz isso por padrão. No MyFaces até dá pra trocar o tipo de serialização, quando se usa STATE_SAVING_METHOD para client, mas nada de gravar no HD. Na minha opinião, gravar os dados no HD não é uma boa solução (pensando em desempenho). Além de recriar toda árvore em cada request, é preciso ler os dados do HD que é lento. Me parece ser mais uma solução para aumentar a disponibilidade. Caso um web-container caia, outro pode pegar os dados do HD. Mas para aumentar a disponibilidade dos dados na sessão (da sessão inteira – não só a árvore de componentes) existem soluções no mundo Java também.

  15. Nico Steppat 09/11/2011 at 17:24 #

    @Victor

    Dá para comprimir a árvore serializada para diminuir a banda mas, como falei no artigo, um tradeoff entre desempenho e uso de memoria e deve ser avaliado para cada projeto.

    abraços

  16. Maciel Bombonato 31/03/2012 at 10:50 #

    Belo post amigo, parabéns.

  17. Priscila 03/04/2012 at 10:15 #

    Bom-dia. Em relação ao JSF, tenha uma dúvida que me perturba.

    Estive atualmente trabalhando numa situação aonde, na página de cadastro do sistema, existem (por exemplo) 3 campos que estão definidos no xhtml como required. Um destes campos são uma combo.

    Ao efetuar a alteração de dados nesta tela, especificamente no campo de combo, se qualquer um dos campos obrigatórios não estiverem preenchidos, ele altera o valor da minha combo para o estado inicial que foi carregado na tela.

    Não encontrei um outra forma de ajustar isso a não ser fazendo a validação dos campos no Bean. Eu não consigo me conformar que o JSF não consiga suprir isso. Pelo menos, ainda não identifiquei uma outra forma para isso.

    Ele guarda o estados das informações, e se der zica em qualquer campo required, ele volta para a informação anterior.

    Não sei se alguém já passou por isso e conseguiu solucionar o problema.

    Mas enfim, valeu o post. Parabéns!

  18. Marcus Aurelius 27/06/2012 at 14:48 #

    Priscila, essa é também minha reclamação com o JSF! Ele começa a fazer as validações na hora que ele quer, e antes de chegar no nosso código! Já mexi um monte com a propriedade “immediate” para alterar a ordem de execução das coisas, mas cheguei à conclusão que não gosto da abordagem do JSF, e prefiro fazer validações manuais também.

  19. João Henrique 10/12/2012 at 11:18 #

    Aqui sempre tem coisas interessantes, dessa de salvar vários estados na sessão do usuário eu não sabia.

    Li novamente essa parte porque estava meio descrente sobre o que estava lendo. Por quê não manter apenas um estado em sessão?

    Pelo menos fica ao nosso critério como usar.

Deixe uma resposta