DEV Community

Cover image for Cache de dados em Java utilizando Proxy Dinâmico e Annotations
josevjunior
josevjunior

Posted on

3 1

Cache de dados em Java utilizando Proxy Dinâmico e Annotations

Durante o desenvolvimento de uma aplicação vão existir diversos momentos que será necessário implementarmos algum tipo de cache. Seja para diminuirmos acessos à algum serviço externo como um banco de dados ou webservice, ou para evitarmos a criação desnecessária de objetos ditos como "caros". Exemplos comuns desse tipo de objeto são EntityManagerFactory, DataSources, etc...

Normalmente, temos que modificar trechos de código para utilizarmos o cache em algum ponto de nosso sistema. Pode ser que seja apenas um simples if ou uma modificação mais complexa, não iremos escapar de "sujar" nosso código com alguma verificação ou tratamento.

No exemplo abaixo temos uma rotina antes e depois da implementação de cache utilizando um HashMap:

Antes do cache

public List<Aviso> getAvisosDoUsuario(String idDoUsuario){
Conexao conexao = null;
Consulta consulta = null;
try {
conexao = conectarNoBancoDeDados();
consulta = realizarConsultaNoBancoDeDados(idDoUsuario);
return mapearResultadoDaConsulta(consulta);
} finally {
fechar(consulta);
fechar(conexao);
}
}

Depois do cache

public List<Aviso> getAvisosDoUsuario(String idDoUsuario){
Conexao conexao = null;
Consulta consulta = null;
try {
List<Aviso> avisos = cacheHashMap.get(idDoUsuario);
if(avisos != null) {
return avisos;
} else {
conexao = conectarNoBancoDeDados();
consulta = realizarConsultaNoBancoDeDados(idDoUsuario);
avisos = mapearResultadoDaConsulta(consulta);
cacheHashMap.put(idDoUsuario, avisos);
return avisos;
}
} finally {
fechar(consulta);
fechar(conexao);
}
}

Pode parecer uma alteração simples, mas dependendo da quantidade de métodos que precisam ser cacheados, como os métodos estão organizados ou como os valores são salvo no cache, pode se tornar complexo. No exemplo anterior o identificador para acessar os avisos é o próprio código do usuário para qual o aviso foi destinado, mas pode haver outras situações em que precisaremos identificar por usuário e data, ou somente por data. No balanço das horas tudo pode mudar.

Para evitarmos incluir esse tipo de regra de interesse transversal no nosso código, vamos utilizar 2 funcionalidades bastante utilizadas no mundo java: os proxies dinâmicos e as annotations.

Proxy Dinâmico

É uma classe que pode implementar um conjunto de interfaces definidas em tempo de execução e, em cada chamada feita a um dos método dessas interfaces, um despachante será chamado e ficará responsável por definir o comportamento da execução do método. Ou seja, teremos controle do próximo método a ser chamada antes da ação acontecer.

Proxies são muito usados no mundo dos frameworks de injeção de dependências. Pois, como não criamos nossos objetos explicitamente através do comando new, isso dá liberdade para eles, internamente, criarem proxies de nossos objetos adicionando funcionalidades a eles que em um primeiro momento pode parecer mágica, como abrir e fechar uma transação no BD durante a execução de um método ou chamar outro trecho de código que "intercepta" nosso objeto.

Neste exemplos não usaremos o mecanismo de proxy nativo da jdk mas sim um provido pela biblioteca cglib. Através dessa lib não ficaremos presos a utilização de proxy dinâmico apenas em interfaces e poderemos utilizá-los em classes concretas.

Annotations

As annotations ou anotações, por outro lado, não foram feitas para mudar o comportamento de um objeto em tempo de execução, mas sim definir informações(metadados) sobre determinado objeto que podem ser obtidas durante sua execução ou compilação. Podemos ver a utilização das anotações sempre que implementamos o método de uma interface ou sobrescrevemos um método de alguma superclasse.

public class MinhaTarefa implements Runnable {
@Override
public void run() {
//
}
}

No caso do @Override, estamos adicionados a informação que o seguinte método está sendo sobrescrito e que não pertence originalmente a essa classe. Tecnicamente isso não altera em nada o comportamento da aplicação, mas através de um proxy é possível verificar se um método, classe, parâmetro ou atributo possui anotações e a partir disso adicionar um comportamento novo de acordo com as informados obtidas.

Implementado nosso cache

Para iniciar, vamos definir nossas annotations e que tipo de informação elas irão nos passar.

package br.com.simplecache.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // Essa anotação será acessada em tempo de exeução
@Target(ElementType.METHOD) // E apenas em métodos
public @interface Cacheable {
/**
* O nome do cache usado como seu indentificador. Caso seja vazio, o padrão
* {NOME_DA_CLASSE}:{NOME_DO_METODO} será assumido. Ex: br.com.service.MyService:doService
* @return
*/
String cacheName() default "";
}
view raw Cachable.java hosted with ❤ by GitHub

Através da anotação Cacheable informaremos qual método gostaríamos de cachear e qual o identificador dele no cache.

Após ter definido o que identificará um método como cacheável, vamos partir para a api responsável por expor os dados cacheados

A classe CacheValue terá o papel de representar um valor cacheado em nossa api, além de conter informações adicionais como a data de criação do valor.

package br.com.simplecache.core;
public class CacheValue {
private long creationTime;
private Object value;
public CacheValue(long creationTime, Object value) {
this.creationTime = creationTime;
this.cacheValue = cacheValue;
}
public long getCreationTime() {
return creationTime;
}
public Object getValue() {
return value;
}
}
view raw CacheValue.java hosted with ❤ by GitHub

Para consultar ou editar esses valores implementaremos a interface CacheManager

package br.com.simplecache.core;
/**
* O gerenciador de cache. Através dos métodos desta interface podemos
* acessar, remover e atualizar valores que desejamos cachear
*/
public interface CacheManager {
/**
* Acessa um valor no cache. Caso o valor não exista, irá return
* null
*
* @param cacheName O identificador do cache
* @param params Um conjunto de parametros que identifica o valor
*
* @return Um valor cacheado ou null
*/
CacheValue getValueFromCache(String cacheName, Object[] params);
/**
* Adiciona ou atualiza um valor no cache
*
* @param cacheName O identificador do cache
* @param params Um conjunto de parametros que identifica o valor
* @param value O valor a ser cacheado
*/
void putValueInCache(String cacheName, Object[] params, Object value);
/**
* Remove um valor no cache
*
* @param cacheName O identificador do cache
* @param params Um conjunto de parametros que identifica o valor
*/
void removeValueInCache(String cacheName, Object[] params);
/**
* Remove todos os valores que corresponda ao valor informado
*
* @param cacheName O identificador do cache
* @param params Um conjunto de parametros que identifica o valor
*/
void removeValuesOfCacheName(String cacheName);
/**
* Remove todos os valores que estão em cache
*/
void removeAll();
}

Por ser uma interface ganhamos certa liberdade para definir o mecanismo/engine de armazenamento de dados. Neste exemplo, vamos salvar os dados em um Map. Se quisermos algo mais elaborado, no futuro basta trocar a implementação para usar algo mais "potente" como o Redis ou até mesmo um SGBD.

A implementação do CacheManager que utilizaremos neste exemplo:

package br.com.simplecache.core;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class SimpleCacheManager implements CacheManager{
private Map<CacheKey, CacheValue> cache;
public SimpleCacheManager() {
cache = new ConcurrentHashMap<>();
}
@Override
public CacheValue getValueFromCache(String cacheName, Object[] cacheKey) {
return cache.get(new CacheKey(cacheName, cacheKey));
}
@Override
public void putValueInCache(String cacheName, Object[] cacheKey, Object value) {
cache.put(new CacheKey(cacheName, cacheKey), new CacheValue(System.currentTimeMillis(), value));
}
@Override
public void removeValueInCache(String cacheName, Object[] params) {
cache.remove(new CacheKey(cacheName, params));
}
@Override
public void removeValuesOfCacheName(String cacheName) {
Set<CacheKey> toRemove = new HashSet<>();
Set<CacheKey> keySet = new HashSet<>(cache.keySet());
for (CacheKey cacheKey : keySet) {
if(cacheKey.cacheName.equals(cacheName)) {
toRemove.add(cacheKey);
}
}
for (CacheKey cacheKey : keySet) {
cache.remove(cacheKey);
}
}
@Override
public void removeAll() {
cache.clear();
}
private static class CacheKey {
private final String cacheName;
private final Object[] cacheKeys;
public CacheKey(String cacheName, Object[] cacheKeys) {
this.cacheName = cacheName;
this.cacheKeys = cacheKeys;
}
@Override
public int hashCode() {
int hash = 7;
hash = 23 * hash + Objects.hashCode(this.cacheName);
hash = 23 * hash + Arrays.deepHashCode(this.cacheKeys);
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final CacheKey other = (CacheKey) obj;
if (!Objects.equals(this.cacheName, other.cacheName)) {
return false;
}
if (!Arrays.deepEquals(this.cacheKeys, other.cacheKeys)) {
return false;
}
return true;
}
}
}

Agora vamos ao proxy...

Como dito anteriormente, utilizaremos a cglib para criar proxy's de classes concretas. Para incluir essa dependência em um projeto maven basta adicionar a dependência abaixo no pom.xml

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.4</version>
</dependency>
Enter fullscreen mode Exit fullscreen mode

Com essa lib no classpath, implementamos a interface net.sf.cglib.proxy.MethodInterceptor, que contêm o método intercept(Object o, Method method, Object[] os, MethodProxy mp). Ele, como o nome diz, irá interceptar qualquer método cuja chamada for feita pela instancia do proxy. Através da classe Method temos acesso ao método que está sendo executado. Utilizaremos essas informações para saber quais serão cacheados ou não. A classe CacheProxy abaixo mostra toda essa lógica:

package br.com.simplecache.core;
import br.com.simplecache.annotations.Cacheable;
import br.com.simplecache.service.MessageService;
import java.lang.reflect.Method;
import java.util.logging.Logger;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
/**
* Toda chamada de método é uma classe 'proxiada' passará pelo método intercept.
* Através dele poderemos controloar o fluxo de execução e com isso salvar seu retorno
* através do CacheManager
*/
public class CacheProxy implements MethodInterceptor {
private static final Logger LOGGER = Logger.getLogger(CacheProxy.class.getName());
private final CacheManager cacheManager;
private final Object delegate;
public CacheProxy(CacheManager cacheManager, Object delegate) {
this.cacheManager = cacheManager;
this.delegate = delegate;
}
@Override
public Object intercept(Object o, Method method, Object[] os, MethodProxy mp) throws Throwable {
// Caso a anotação esteja presente, criamos o cache
if(method.isAnnotationPresent(Cacheable.class)) {
LOGGER.info("Executando método cacheavel: " + method.getName());
Cacheable cacheableAnnotation = method.getAnnotation(Cacheable.class);
String cacheName = null;
// Caso o cache não tenha nome vamos salvar o padroa {NOME_DA_CLASS}:{NOME_DO_METODO}
if("".equals(cacheableAnnotation.cacheName())) {
StringBuilder str = new StringBuilder();
str.append(method.getDeclaringClass().getName());
str.append(":");
str.append(method.getName());
cacheName = str.toString();
} else {
cacheName = cacheableAnnotation.cacheName();
}
LOGGER.info("Consultando valor no cache...");
CacheValue cacheValue = cacheManager.getValueFromCache(cacheName, os);
if(cacheValue != null) {
LOGGER.info("Retornando valor cacheado");
return cacheValue.getCacheValue();
}
LOGGER.info("Não foi encontrado nenhum valor no cache. Executando método original...");
Object result = method.invoke(delegate, os);
LOGGER.info("Salvando retorno no cache...");
cacheManager.putValueInCache(cacheName.toString(), os, result);
return result;
}
return mp.invoke(delegate, os);
}
public static <T> T newCacheableProxy(CacheManager cacheManager, T instance) {
if(cacheManager == null) {
throw new NullPointerException("O gerenciado de cache deve ser diferente de null");
}
if(instance == null) {
throw new NullPointerException("A instancia a ser proxiada deve ser diferente de null");
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(instance.getClass());
enhancer.setCallback(new CacheProxy(cacheManager, instance));
return (T) enhancer.create();
}
}
view raw CacheProxy.java hosted with ❤ by GitHub

O trecho seguinte mostra o método que utilizaremos para criar os proxys. Nele vemos como definimos a classe que será 'proxiada' e como vinculamos o ´MethodInteceptor´.

public static <T> T newCacheableProxy(CacheManager cacheManager, T instance) {
if(cacheManager == null) {
throw new NullPointerException("O gerenciado de cache deve ser diferente de null");
}
if(instance == null) {
throw new NullPointerException("A instancia a ser proxiada deve ser diferente de null");
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(instance.getClass());
enhancer.setCallback(new CacheProxy(cacheManager, instance));
return (T) enhancer.create();
}

Utilizando nosso cache

Vamos criar uma classe fictícia responsável por consultar as mensagens disponíveis para cada usuário e definir um de seus método com a anotação @Cacheable

package br.com.simplecache.service;
import br.com.simplecache.annotations.Cacheable;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
import java.util.stream.Collectors;
public class MessageService {
private static final List<Message> MESSAGES = new ArrayList<Message>();
private static final Logger LOGGER = Logger.getLogger(MessageService.class.getName());
static {
MESSAGES.add(new Message(1L, "Mensagem Importante", "A vingança nunca é plena, mata a alma e a envenena"));
MESSAGES.add(new Message(1L, "Mensagem Importante", "Pessoas boas devem amar seus inimigos"));
}
@Cacheable(cacheName = "USER_MESSAGE")
public List<Message> getMessagesOfUser(Long userId) {
LOGGER.info("Acessando messagens do usuário...");
return MESSAGES.stream()
.filter(message -> message.getIdUsuario().equals(userId))
.collect(Collectors.toList());
}
}

Os valores serão armazenados em estrutura de Map (Chave => Valor) da seguinte forma:

       CHAVE                  VALOR
 ------------   ---
|"USER_MESSAGE"| 1 | => [MENSAGEM_1, MENSAGEM_2]   
 ------------   ---

 -------------- ---
|"USER_MESSAGE"| 2 | => []   
 -------------- ---
Enter fullscreen mode Exit fullscreen mode

O cacheName definido na anotação, em conjunto com os valores dos parâmetros, serão usados como identificadores para salvar o retorno do método em cache. Dessa forma, cada chamada ao método utilizando um valor diferente no parâmetro será um "registro" cacheado.

Alguns testes foram feitos para validar o funcionamento do nosso mecanismo de cache.

set 05, 2021 6:26:22 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Executando método cacheavel: getMessagesOfUser
set 05, 2021 6:26:22 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Consultando valor no cache...
set 05, 2021 6:26:22 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Não foi encontrado nenhum valor no cache. Executando método original...
set 05, 2021 6:26:22 PM br.com.simplecache.service.MessageService getMessagesOfUser
INFORMAÇÕES: Acessando messagens do usuário...
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Salvando retorno no cache...
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Executando método cacheavel: getMessagesOfUser
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Consultando valor no cache...
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Retornando valor cacheado
É a mesma lista: true
set 05, 2021 6:26:23 PM br.com.simplecache.core.SimpleCacheManager removeAll
INFORMAÇÕES: Limpando todo o cache
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Executando método cacheavel: getMessagesOfUser
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Consultando valor no cache...
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Não foi encontrado nenhum valor no cache. Executando método original...
set 05, 2021 6:26:23 PM br.com.simplecache.service.MessageService getMessagesOfUser
INFORMAÇÕES: Acessando messagens do usuário...
set 05, 2021 6:26:23 PM br.com.simplecache.core.CacheProxy intercept
INFORMAÇÕES: Salvando retorno no cache...
É a mesma lista: false
Enter fullscreen mode Exit fullscreen mode

Neste post quis mostrar uma forma de implementar um cache um pouco mais sofisticado, mas sem partir para a utilização de um container CDI, um servidor de aplicação ou migrar para o Spring, já que em sistemas legados não temos tanta liberdade para implementar esse tipo de solução e essa seria um proposta menos invasiva.

Repositório com os fontes: https://github.com/josevjunior/simple-cache-manager

Referências

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Engage with a sea of insights in this enlightening article, highly esteemed within the encouraging DEV Community. Programmers of every skill level are invited to participate and enrich our shared knowledge.

A simple "thank you" can uplift someone's spirits. Express your appreciation in the comments section!

On DEV, sharing knowledge smooths our journey and strengthens our community bonds. Found this useful? A brief thank you to the author can mean a lot.

Okay