terça-feira, 16 de dezembro de 2014

Cache com Escopo de Requisição Utilizando os Interceptadores do CDI


Uma das particularidades do JSF com o Facelets é que um método get de um Bean pode ser executado mais de uma vez em uma requisição. Esse número pode aumentar quando o método for chamado nas propriedades rendered dentro de um h:datatable ou um ui:repeat.

É um comportamento bastante conhecido e o ideal é que não se coloque regras de negocio nesses métodos ou se faça cache no Managed Bean. Isto é muito comum quando precisamos executar um método para cada linha de um datatable, como no exemplo abaixo:



Neste caso o botão de exclusão é condicionado a uma regra que está no Managed Bean. Existem algumas alternativas para resolver o problema que é causado pelo método ser chamado diversas vezes e a mais comum é criar um cache no Managed Bean.

Um map pode ser utilizado para guardar os valores do que já foi executado. Isto é muito utilizado, mas faz com que o seu cache fique com o escopo do MB e isto nem sempre isto é o ideal.

Para simulação criei o uma lista com 15 elementos e fiz com que o metodo possuiPermisão retornasse true para os elementos que possuam id par:

 
 @PostConstruct
 public void init() {
  produtos = new ArrayList(15);
  for (int i = 1; i <= 15; i++) {
   Produto p = new Produto();
   p.setId((long) i);
   p.setNome("Produto " + i);
   Double valor = Math.random() * 50;
   p.setValor(valor);
   produtos.add(p);
  }
 }
 
 public boolean possuiPermissao(Produto p) {
  sleep();
  boolean resultado = p.getId() % 2 == 0 ? true : false;
  log.info(MessageFormat.format("possuiPermissao(Produto {0})",p.getId()));
  return resultado;
 } 

Ao acessar a página aparece isto no log:

21:01:22,158 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 1)
21:01:22,181 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 2)
21:01:22,202 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 2)
21:01:22,223 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 2)
21:01:22,244 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 2)
21:01:22,264 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 2)
21:01:22,287 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 3)
21:01:22,309 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 4)
21:01:22,329 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 4)
21:01:22,349 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 4)
21:01:22,370 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 4)
21:01:22,390 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 4)
21:01:22,412 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 5)
21:01:22,434 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 6)
21:01:22,454 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 6)
21:01:22,475 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 6)
21:01:22,496 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 6)
21:01:22,517 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 6)
21:01:22,541 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 7)
21:01:22,563 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 8)
21:01:22,584 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 8)
21:01:22,605 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 8)
21:01:22,637 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 8)
21:01:22,658 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 8)
21:01:22,680 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 9)
21:01:22,702 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 10)
21:01:22,723 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 10)
21:01:22,743 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 10)
21:01:22,764 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 10)
21:01:22,784 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 10)
21:01:22,806 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 11)
21:01:22,828 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 12)
21:01:22,849 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 12)
21:01:22,869 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 12)
21:01:22,890 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 12)
21:01:22,910 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 12)
21:01:22,933 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 13)
21:01:22,955 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 14)
21:01:22,976 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 14)
21:01:22,997 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 14)
21:01:23,018 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 14)
21:01:23,038 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 14)
21:01:23,060 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 15)

Podemos notar que o método foi executado uma vez para os objetos que ele retorna false e cinco vezes para o que retorna true. Isto é um comportamento esperado e vocês podem ver uma explicação neste link aqui.

Os interceptadores do CDI permitem que a execução de um método seja executada e isto é muito útil em vários cenários, desde controle de acesso a nível de método até logar todos os métodos de um bean medindo o tempo de execução.

Vamos precisar de duas classes, para criar o nosso interceptador e utilizar eles no exemplo: RequestCache.java e RequestCacheInterceptor.java

A primeira classe é a definição na anotação que vamos utilizar para marcar os métodos que queremos fazer cache:

 
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequestCache {

}

A segunda classe implementa o interceptador e eu utilizei o próprio request para fazer o cache guardando um Map. A classe abaixo não está completa, mas da para entender todo o processo e no final do artigo deixarei um link com o projeto eclipse deste exemplo.

@Interceptor
@RequestCache
public class RequestCacheInterceptor implements Serializable {
 
 private static final long serialVersionUID = 1L;
 private static final String MAP_NAME = RequestCacheInterceptor.class.getName() + "_map_cache";

 @AroundInvoke
 public Object execute(InvocationContext invocationContext) throws Exception {
     Map mapCache = getMapCache();
     Key key = new Key(invocationContext);
     Result result = mapCache.get(key);
     if (result == null) {
         long ini = System.currentTimeMillis();
         result = new Result(invocationContext.proceed());
         result.time = System.currentTimeMillis() - ini;
         mapCache.put(key, result);
     }
     result.count++;
     return result.object;
 }   

 @SuppressWarnings("unchecked")
 private Map getMapCache() {
     Map map = (Map) getRequest().getAttribute(MAP_NAME);
     if (map == null) {
         map = Collections.synchronizedMap(new HashMap());
         getRequest().setAttribute(MAP_NAME, map);
     }
     return map;
 }


Toda vez que o nosso método, marcado com a anotação @RequestCache, for executado o método execute será chamado e método passará a ser executado somente quando for executada o método invocationContext.proceed().

Apliquei a anotação, reiniciei o Jboss e executei novamente a página:

 
 @RequestCache
 public boolean possuiPermissao(Produto p) {
  sleep();
  boolean resultado = p.getId() % 2 == 0 ? true : false;
  log.info(MessageFormat.format("possuiPermissao(Produto {0})",p.getId()));
  return resultado;
 }

21:21:51,339 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 1)
21:21:51,361 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 2)
21:21:51,385 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 3)
21:21:51,407 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 4)
21:21:51,432 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 5)
21:21:51,454 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 6)
21:21:51,480 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 7)
21:21:51,502 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 8)
21:21:51,526 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 9)
21:21:51,548 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 10)
21:21:51,573 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 11)
21:21:51,596 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 12)
21:21:51,619 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 13)
21:21:51,641 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 14)
21:21:51,665 INFORMAÇÕES [ProdutoMB] (http-...) possuiPermissao(Produto 15)

Com isso podemos ver que nosso interceptador funcionou perfeitamente. Podemos usar em vários pontos do projeto e com isto economizamos códigos repetitivos.

Neste exemplo utilizei um projeto JSF criado pelo Eclispe e depois Mavenizado pelo Configure / Convert to Maven Project.

Utilizei o Jboss 7.1 o download do projeto pode ser feito aqui.

Em breve colocarei este exemplo no Github.