Back-End

26 jul, 2016

Java EE 8 – Utilizando o JCache para suas aplicações

Publicidade

Neste artigo, serão abordadas as vantagens e as especificidades do uso do JCache, tecnologia utilizada em diversas aplicações dentro do Moip.

Flat-baners-moip

O que é o cache e por que utilizá-lo

Basicamente, o termo cache pode ser compreendido como uma área de armazenamento onde dados ou processos frequentemente utilizados são guardados para um acesso futuro mais rápido, poupando tempo e uso desnecessário do seu hardware.

Muitas das aplicações no Moip utilizam o processo de Cache e resiliência de dados.

As estratégias de Cache, em geral, são utilizadas para melhorar a performance de suas aplicações, caso sua base de dados ou alguma API esteja indisponível ou, ainda, simplesmente para poder escalar suas aplicações otimizando tempo de request para outros serviços. O Cache, às vezes, pode ser considerado uma maneira de resiliência da sua aplicação para algum serviço externo, nos quais os dados pouco mudam e tenham um tempo de resposta além do esperado, tornando inviável a constante requisição.

Por isso, a utilização de uma estratégia de Cache se torna algo que deveria ser considerado ao escrever aplicações. O Cache é algo tão importante para o desempenho de sua aplicação, que empresas como Netflix, Spotify e Shopify criaram e investem em diversas ferramentas, a fim de otimizar e manter um serviço de qualidade.

Sobre a especificação

A JSR – 107 descreve a API e a semântica para acesso na memória Cache de objetos, incluindo a criação, acesso compartilhado, invalidação e a coerência entre JVMs. Até a criação do JSR, o mais popular framework para Cache era o EHCache, que a maioria dos desenvolvedores deve ter utilizado junto com o Hibernate fazendo Cache de consultas em banco de dados, evitando assim que queries consideradas pesadas fossem executadas constantemente.

Hoje o próprio EHCache já tem sua versão oficial de implementação da JSR, mas há outras implementações como: Oracle Coherence, Hazelcast, Infinispan e Apache Ignite. Podem ser encontrados mais detalhes sobre essas versões em https://jcp.org/aboutJava/communityprocess/implementations/jsr107/index.html.

JCache Componentes

A API por si só já é bem intuitiva, facilitando muito o seu uso. A seguir, serão listados alguns dos seus principais componentes:

  • Cache Provider: controla o Cache Manager e lida com vários deles
  • Cache Manager: lê, cria, atualiza e destrói informações no Cache
  • Cache: armazena os dados reais e disponibiliza uma interface CRUD para lidar com as Entries
  • Entry: abstração key-value muito parecida com java.util.Map

cache-1

Figura 1.0 – Estrutura da JSR – 107

Brincando com o JCache

Como implementação da JSR será utilizada a EHCache, pois é uma das bibliotecas mais maduras, afinal, já foi amplamente utilizada, mas somente a partir da versão 3 que implementaram seguindo a especificação, lembrando que pode ser escolhida qualquer uma das implementações.

As dependências necessárias para utilizar a JSR são as seguintes:

<dependency>
 <groupId>javax.cache</groupId>
 <artifactId>cache-api</artifactId>
 <version>1.0.0</version>
</dependency>

<dependency>
   <groupId>org.ehcache</groupId>
   <artifactId>ehcache</artifactId>
   <version>3.0.0</version>
</dependency>

Dado que foram importados os jars corretamente, será criado um main bem simples, ilustrando um exemplo programático de como utilizar o JCache:

package com.github.brenooliveira.samples;

import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;

public class JCacheUsageSample1 {
   public static void main(String[] args){
       CachingProvider jcacheProvider = Caching.getCachingProvider();
       CacheManager jcacheManager = jcacheProvider.getCacheManager();
       MutableConfiguration<String, String> jcacheConfig = new MutableConfiguration();
       jcacheConfig.setTypes(String.class, String.class);
       Cache<String, String> cache = jcacheManager.createCache("sampleCache", jcacheConfig);

       String key = "key1";
       cache.put(key, "Hello JCache");
       System.out.println("=========> " + cache.get(key));
   }
}

Nesse simples exemplo, foi utilizada toda uma estrutura do JCache, conforme demonstra na figura 1.0. Após ser executado o programa acima, aparecerá na saída de console o seguinte:

=========> Hello Cache

Primeiramente, foi criado um Provider e, na sequência, foi pega uma instância do CacheManager a partir do Provider.

Em seguida, foi informado qual tipo de informação será armazenada no Cache; para isso, foi utilizada uma instância MutableConfiguration. Então, será criado um Cache onde será informada uma chave e passará a configuração dos dados que serão mantidos no Cache.

Agora, sim, podem ser feitas operações com o Cache – observe que as operações são semelhantes às do java.util.Map(.put, .delete, .get, .replace). Enfim, foram escritos os primeiros dados no nele.

Utilizando junto com CDI e anotações

O exemplo anterior é bem simples e prático para demonstrar como utilizar todos os componentes do JCache, mas ele dificilmente será utilizado dessa maneira em produção.

Sendo assim, ele será integrado ao CDI. Para isso, serão importadas as seguintes dependências:

<dependency>
   <groupId>org.jboss.weld.se</groupId>
   <artifactId>weld-se-core</artifactId>
   <version>3.0.0.Alpha15</version>
</dependency>

A JSR também traz um conjunto de anotações que serão utilizadas. As anotações do javax.cache.annotation.* foram criadas para trabalhar em conjunto com um DI (dependency injection), e podem ser escolhidas como Guice ou Spring.

Primeiramente, os interceptors do JCache precisam ser registrados no CDI para que as anotações possam funcionar. Para isso, basta criar em resources/META-INF/beans.xml e adicionar o seguinte conteúdo:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                 http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="all">

   <interceptors>
       <class>org.jsr107.ri.annotations.cdi.CacheResultInterceptor</class>
       <class>org.jsr107.ri.annotations.cdi.CacheRemoveEntryInterceptor</class>
       <class>org.jsr107.ri.annotations.cdi.CacheRemoveAllInterceptor</class>
       <class>org.jsr107.ri.annotations.cdi.CachePutInterceptor</class>
   </interceptors>
</beans>

Agora, com os interceptors do JCache devidamente registrados no CDI, podem ser utilizadas as anotações definidas pela especificação que os interceptors farão a mágica acontecer.

Conhecendo as principais anotações da especificação:

  • javax.cache.annotation.CacheDefaults
  • javax.cache.annotation.CacheKey
  • javax.cache.annotation.CacheResult
  • javax.cache.annotation.CacheValue
  • javax.cache.annotation.CachePut
  • javax.cache.annotation.CacheRemove
  • javax.cache.annotation.CacheRemoveAll

A anotação @CacheDefaults é declarada como anotação de classe, que permite declarar o nome default do Cache.

import javax.cache.annotation.*;

@CacheDefaults(cacheName = "alunoCache")
public class CacheBean {
}

A anotação @CachePut permitem escrever em uma chave. O valor desejado para ser gravado no Cache é muito parecido com a classe javax.util.Map, como já mencionado anteriormente. Aqui, também, podem ser declaradas as chaves e os valores que serão inseridos no Cache.

Em seguida, um objeto Aluno será colocado em um objeto javax.cache.Cache chamado alunoCache com uma id. Pode ser mantido o corpo do método vazio ou continuar com os parâmetros em qualquer lugar (DB ou NoSQL, por exemplo), no corpo do método – isso pode variar de acordo com suas necessidades.

package com.github.brenooliveira.samples;

import javax.cache.annotation.*;

@CacheDefaults(cacheName = "alunoCache")
public class CacheBean {

   @CachePut
   public void put(@CacheKey Long id, @CacheValue Aluno aluno) {

   }
}

Quanto à anotação @CacheResult, é a maneira utilizada para recuperar dados armazenados no cache, se informada uma chave que exista nele. Caso o valor relacionado já esteja presente, o valor em Cache é retornado do método de proxy sem entrar corpo do método, se não, o fluxo de execução entra no corpo e também coloca objeto retornado para o Cache.

package com.github.brenooliveira.samples;

import javax.cache.annotation.*;

@CacheDefaults(cacheName = "alunoCache")
public class CacheBean {

   @CacheResult
   public Aluno get(@CacheKey Long id) {
       final Aluno person = new Aluno(id);
       aluno.updateStatus();
       return person;
   }
}

Com a anotação @CacheRemove, os dados em chave podem ser apagados. Também existe o @CacheRemoveAll, que invalida todo os dados no cache.

package com.github.brenooliveira.samples;

import javax.cache.annotation.*;

@CacheDefaults(cacheName = "alunoCache")
public class CacheBean {

   @CacheRemove
   public void invalidate(@CacheKey Long id) {

   }

   @CacheRemoveAll
   public void invalidateAll() {

   }
}

Assim, sua classe, com o gerenciamento do Cache, está disponível para ser utilizada por diversas partes da aplicação, bastando apenas injetar o objeto CacheBean na classe desejada, para isso, pode operar de maneira simples como o próprio CDI sugere. O seguinte trecho de código exemplifica:

@Inject
CacheBean cacheBean;

Conclusão

Até ao final da JSR, Java não tinha um padrão para lidar com Cache em sua aplicação. Uma boa estratégia de cache deve ser pensada já na concepção de suas aplicações, principalmente se a aplicação for a necessidade de grandes acessos à base de dados, web services etc.

O JCache foi desenvolvido para trabalhar em conjunto com o CDI, pois juntos facilitam muito a utilização e a injeção de seu Cache em diversos componentes. A JSR também provê um conjunto de anotações que tornam o uso bem simples e intuitivo, tendo fácil utilização.

Referências