@ViewScoped, o ovo e a galinha

Uma das funcionalidades mais esperadas do JSF2 sem dúvida foi o escopo de visão (view scope), mais conhecido como @ViewScoped. Com esta simples anotação se tornou possível manter managed beans num escopo maior do que requisição e menor do que sessão. O que resolveu a maioria dos problemas relacionados a ajax, uso inadequado do escopo de sessão e idas e vindas desnecessárias ao banco de dados.

Apesar de todas as coisas boas trazidas pelo @ViewScoped, ele trouxe consigo alguns problemas e limitações que a maioria dos desenvolvedores não estavam acostumados, como problemas de serialização, escopo restrito apenas a mesma página, integração precária com CDI e Spring, incompatibilidade com JSTL e a necessidade de limpar a todo instante os dados do managed bean.

Entre estes problemas, o que gerou bastante frustração para muitos foi o uso do @ViewScoped juntamente com component bindings ou tag handlers (Issue #1492), tanto é que o problema acabou recebendo um apelido interessante, “chicken/egg issue“, ou em tradução livre, o problema do ovo e da galinha. Esse bug ficou mais conhecido durante os primeiros anos de vida do JSF2, mas hoje, após a sua correção, ele não é tão recorrente assim.

O problema do ovo e da galinha

O bug causa a instanciação dos managed beans em @ViewScoped a cada requisição, seja ela ajax ou não. A princípio isso não parece muito sério, mas vai de contra a natureza do escopo, que deveria ser instanciado uma única vez. Isso pode acarretar problemas como processamento desnecessário de métodos @PostConstruct, erros em conversores e validadores ou problemas mais difíceis de detectar, como a criação da árvore de componentes em um estado inválido.

O erro se manifesta ao “linkar” (fazer binding) um bean em @ViewScoped com tag handlers ou component bindings (os atributos id e binding dos componentes) e ter o Partial State Saving habilitado. Entenda tag handlers como qualquer tag que não seja um componente (UIComponent), elas são facilmente reconhecidas por não terem o atributo rendered, como por exemplo JSTL.

Um caso bem comum onde o problema se manifesta é quando você tenta utilizar a tag handler ui:include (não, ela não é um componente!) com @ViewScoped:

<ui:include src="#{menuViewScopedBean.menuName}.xhtml" />

Ou até em casos aparentemente inofensivos, como iterar uma coleção com JSTL, como abaixo:

<c:forEach var="car" items="#{carViewScopedBean.allCars}">
	<h:outputText value="#{car}" />
</c:forEach>

Não esqueça que component bindings (os atributos id e binding são avaliados em build time) também disparam o problema:

<h:form binding="#{viewScopedBean.uiForm}">
	…
</h:form>

Segue a lista das tag handlers que usamos ao trabalhar com JSF. Logo você deveria evita-las ao usar @ViewScoped:

Entendendo o que acontece

Tag handlers e component bindings são avaliados durante a fase de (re)construção da árvore de componentes (build time), enquanto os componentes do JSF são avaliados somente na fase de renderização (render time).

O problema é que beans @ViewScoped são armazenados na árvore de componentes, mas a árvore só estará disponível após a fase de construção, e a tag handler precisa do bean para construir a árvore (por isso o apelido do bug, “chicken/egg issue“).

Como tag handlers são executadas antes da árvore estar disponível e, existe um binding da tag com o bean, o JSF é obrigado a instanciar um novo bean (com as propriedades não inicializadas) para satisfazer a EL (binding) da tag handler, quando na verdade o que ele deveria ter feito era referenciar o bean que já estava armazenado na árvore. Basicamente você acaba com duas instâncias do managed bean por requisição. Onde o primeiro é usado na reconstrução da árvore de componentes e o segundo durante o ciclo de vida do JSF.

No fim de tudo, após a árvore ter sido reconstruída e o bean ter sido colocado de volta no escopo de visão (view scope), sobrescrevendo o bean que foi criado, o ciclo de vida continua normalmente, porém os danos causados pelo bug já aconteceram e não tem como voltar atrás.

Soluções

Existem algumas soluções para o problema, algumas simples e outras nem tanto. A notícia boa é que para quem já utiliza JSF 2.2 o problema já foi resolvido e não precisa se preocupar com ele!

Mas como bem sabemos, nem todos os projetos podem se dar ao luxo de se manterem atualizados a cada nova release, seja por qual for o motivo. Sendo, segue as principais soluções:

1. Atualize a versão do JSF

Como disse, o bug foi mais crítico nos primeiros anos do JSF 2.0 e 2.1, mas hoje em dia ele só atormenta quem não se manteve atualizado com as releases periódicas do Mojarra e/ou Myfaces.

O bug foi resolvido juntamente com a release do JSF 2.2, logo, quem já trabalha com a versão mais recente do faces não terá do que reclamar. Para quem ainda trabalha com JSF 2.1 existe a correção desde a versão 2.1.18 do Mojarra, qualquer versão antes desta é totalmente bugada e apresentará todos os problemas relatados.

Para quem não pode atualizar a versão do faces, as próximas soluções podem ajudar.

2. Desligue o Partial State Saving

Nas primeiras semanas em que o bug apareceu, a solução mais simples, porém a mais radical para aqueles que não poderiam abrir mão de suas tag handlers era desligar o Partial State Saving (PSS) globalmente, já que o problema estava intimamente ligado à ele. Para isso, basta configurar seu web.xml com:

<context-param>
	<param-name>javax.faces.PARTIAL_STATE_SAVING</param-name>
	<param-value>false</param-value>
</context-param>

Desligar o PSS é abrir mão de uma das features mais importantes do JSF2, pois ela é responsável por manter a árvore de componentes parcialmente em memória, com isso ela diminui consideravelmente o consumo de banda de rede, memória e cpu do servidor. Ao desligar o PSS a aplicação volta a trabalhar como no JSF 1.2, ou seja, armazenando toda a árvore de componentes em memória. E isto pode ser um problema se sua aplicação mantém o estado da árvore no lado cliente.

3. Desligue o Partial State Saving por página

Uma solução melhor do que desligar o PSS globalmente, é desliga-lo somente para as páginas (views) problemáticas, as páginas que utilizam tag handlers ou component bindings. Dessa forma sua aplicação, certamente a maior parte dela, continua tirando proveito das otimizações do JSF 2. Para isso, basta configurar seu web.xml com:

<context-param>
	<param-name>javax.faces.FULL_STATE_SAVING_VIEW_IDS</param-name>
	<param-value>/home.xhtml,/pages/booking.xhtml</param-value>
</context-param>

O parâmetro aceita uma lista de views (páginas) separadas por vírgula na qual terão o PSS desligado.

4. Substitua tag handlers por componentes

Se por acaso você não quiser abrir mão do PSS então a única solução que lhe resta é substituir todas as tag handlers que tem binding com beans @ViewScoped por componentes normais do JSF.

Para isso, segue uma listas com alternativas para cada tag handler:

  • <c:choose>: use o atributo rendered no lugar dele
  • <c:forEach>: substitua pelo componente <ui:repeat>
  • <c:if>: use o atributo rendered no lugar dele
  • <c:set>: substitua por <ui:param>, <f:viewParam>, @ManagedProperty ou @PostConstruct
  • <f:actionListener>: use o atributo actionListener do componente em vez dele
  • <f:convertXxx> como <f:convertNumber>: use o atributo converter do componente ou use o <f:converter> em vez dele
  • <f:facet>: não existe alternativa para ele, simplesmente não use EL em qualquer atributo desta tag handler
  • <f:validateXxx> como <f:validateLongRange>: use o atributo validator ou use o <f:validator> em vez dele
  • <f:valueChangeListener>: use o atributo valueChangeListener no lugar dele
  • <ui:decorate>: não existe alternativa para ele, simplesmente não use EL em qualquer atributo desta tag handler
  • <ui:composition>: não existe alternativa para ele, simplesmente não use EL em qualquer atributo desta tag handler
  • <ui:include>: use componentes <ui:fragment rendered> para cada <ui:include> com páginas estáticas
  • qualquer tag file customizada: substitua por componentes do JSF (UIComponent)

Concluindo

Hoje em dia o problema não é dos mais críticos, mas ainda existem muitas aplicações que sofrem com ele. Se você tem um projeto com versões antigas do JSF 2, é bem provável que o problema aconteça em algumas de suas páginas, mas talvez não chegue a ser algo sério e você não tenha percebido.

Tão importante quanto resolver o problema é entendê-lo, o que leva muitas vezes ao aprendizado de novos conceitos e de como o JSF trabalha por debaixo dos panos. Conceitos e discussões sobre este problema e outros são tratados no curso de JSF 2 e Spring da TriadWorks.

E aí, você já conhecia o problema do ovo e da galinha? Usou alguma outra solução que não comentei?