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

16 thoughts on “@ViewScoped, o ovo e a galinha

  1. Muito bom o post Ponte, bem esclarecedor.

    outra solução seria usar o ViewAccessScoped do CODI(https://cwiki.apache.org/confluence/display/EXTCDI/JSF+Usage#JSFUsage-ViewAccessScope)

    ele estende um pouco o conceito de view, na verdade é uma conversação que fica ativa enquanto houver referencia ao managed bean via EL podendo inclusive montar wizards ou fluxos sem muito trabalho. Outra vantagem é que ele trabalha bem com “back button” do browser, eu lembro que qdo usava os escopos do faces apanhava bastante pra isso.

    Parabens tche!

    • Oi Pestano,

      Excelente! O @ViewAccessScoped funciona semelhante a um dos escopos do Myfaces Orchestra, os objetos sobrevivem enquanto houver referência à eles nas páginas.

      Acredito que outros escopos de conversação também devam resolver bem o problema. Não fiz os testes, mas acredito que o t:saveState do Myfaces Tomahawk também resolva o problema.

      Enfim, muitos dos problemas do JSF são corrigidos ou passados por cima quando se utiliza frameworks de terceitos, como CODI, Seam etc.

      Obrigado pelo comentário, Pestano. Tenho certeza que ajudará muita gente!

  2. Já viram a seguinte opção:

    FacesContext.getCurrentInstance().isPostback()

    Com essa propriedade é possível englobar os métodos que são invocados constantemente e se não for um postBack vocês podem ignorar a execução dos mesmos, por exemplo:

    if (!isPostback()) {
    metodoX();
    }

    É uma prática muito comum no .net e como tive esse problema de versão apliquei no jsf e funciona da mesma forma.

  3. Rafael, esse artigo me ajudou a entender muito dos problemas “inexplicáveis” que aconteciam em algumas páginas do meu projeto. Obrigado pelas explicações!

  4. Excelente explicação Rafael, tenho a impressão que este bug ainda não foi completamente corrigido, na versão do mojarra 2.3.0, vou até realizar testes em versões mais novas, porém eu acabei criando o que considero uma gambiarra master para resolver este problema, e sua explicação explica os bugs com claresa, isto porque desenvolvi um framework JSF open source, que utiliza apenas um objeto Input para exibição de qualquer tipo de campo de formulário, o objeto input possui todas as propriedades para exibição do componente,
    A ideia era a taglib input, realizar um simples include de xhtml de acordo com o tipo do componente.
    Porém coisas estranhas acontece: por exemplo: ao exibir os inputs em uma data grid, todos os outros inputs recebem o mesmo xhtml de exibição do primeiro, e isto deve acontecer devido ao ciclo de vida do Ui ser anterior a renderização dos componentes, e não se atualizar a cada nova renderização de novo componente.
    Meu tagLib fica parecido com isso:

    … e assim vai por mais 15 tipos de componentes de input de acordo com o tipo de informação
    como você sugere no post, estou substituindo o include com fragment rendered = false…
    Mas isso funciona mas é muito deselegante como diria Fatima Bernades, e estou aguardando aparecer uma solução para isso a 1 ano..

    Existe uma forma de injetar um include como se ele fosse um componente, e contornar este problema? tipo uma implementação de include na mão, direto na classe do componente?

    • Opa,

      Acho dificil o problema ainda existir nas versões mais recentes. O seu código não apareceu nos comentários, talvez seja melhor você colar o link de um Gist (gist.github.com) com o código, facilitando assim o entendimento.

      Não sei se entendi bem o problema, mas quando colocamos um componente dentro de uma componente de iteração (como h:dataTable ou ui:repeat), este componente existe somente 1x dentro da árvore de componentes mas ele é renderizado N vezes durante o processamento do componente, ou seja, quando se guarda ou carrega o estado do componente de iteração durante o ciclo de vida. Nesse caso, até onde sei, seu componente deve estar preparado para este tipo de situação, mas não tenho experiência com a criação de componentes customizados para ser mais assertivo.

Leave a Reply

Your email address will not be published. Required fields are marked *