Spring AOP: Cacheando aplicaciones usando anotaciones y aspectos con Aspectj

2
18539



Spring AOP: Cacheando aplicaciones usando anotaciones y aspectos con Aspectj













Spring AOP: Cacheando aplicaciones usando anotaciones y aspectos con Aspectj

Introducción.

Para empezar este tutorial, os invito a observar la siguiente clase Java poniendo especial atención a los métodos getAll y add.

package com.autentia.tutoriales.spring.aop.aspectj;

import com.autentia.tutoriales.spring.aop.aspectj.cache.Cachea;
import com.autentia.tutoriales.spring.aop.aspectj.cache.Descachea;

/**
 * Bean de nuestra aplicación con métodos que cachean y descachean
 * @author Carlos García. Autentia.
 * @see http://www.mobiletest.es
 */
public class Provincias  {
	private java.util.List provincias;
	
	/**
	 * Constructor
	 */
	public Provincias(){
		this.provincias = new java.util.ArrayList();
	}

	/**
	 *(SIMULAMOS UNA OPERACION COSTOSA CACHEAMOS EL RESULTADO CON TIEMPO INFINITO)
	 * @return Devuelve todas las provincias 
	 */
	@Cachea(cacheKey="Provincias.provincias", expireTime=0)
	public java.util.List getAll(){
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {}
		
		return provincias;
	}
  	
	/**
	 * Añadimos una provincia (LIMPIAMOS LA CACHE)
	 * @param provincia Provincia a añadir
	 */
	@Descachea(cacheKey="Provincias.provincias")
	public void add(String provincia){
		provincias.add(provincia);
	}
}

Ahora bien al igual que pasa en la mayoría de las aplicaciones, hay muchos métodos que siempre devuelven lo mismo (getAll) a no ser que otro método (add) invalide el mismo.

Estos métodos son claros candidatos de ser cacheados, pues ¿para qué volver a consultar a la BD y traernos los resultados por la red?,
¿para que volver a invocar un servicio web si estamos seguros de que el resultado será el mismo?. ¿Para qué….?

Pues bien, en este tutorial vamos a usar la programación orientada a aspectos para dotar con un par de simples anotaciones los métodos cuyo resultado queremos cachear y los métodos que invalidan la cache(s) establecidas.

No os voy a tratar temas teóricos sobre programación orientada a Aspectos, Spring, Maven… para eso están los libros, Internet o
los cursos que impartimos en Autentia, sólo os quiero presentar un completo ejemplo práctico de creación de un aspecto basado en Aspectj y anotaciones.

Más adelante escribiremos un test con JUnit que invoque los métodos getAll y add, observe cual será la salida de la aplicación:

............
Llamada add para forzar el descacheo
Llamada getAll: 2010 milisegundos
Llamada getAll: 0 milisegundos
Llamada add para forzar el descacheo
Llamada getAll: 2002 milisegundos
FIN
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.969 sec
............

Observe lo siguiente:

  • En la segunda invocación del método getAll nos ahorramos unos segundos de ejecución
    y recursos.. que hoy en día las aplicaciones a veces van más lentas que hace 8 años usando en la actualidad máquinas más potentes 😉
  • Posteriormente, una vez descacheada la información, el tiempo de ejecución de getAll vuelve a incrementarse, es decir, la caché fue limpiada.

Manos a la obra con el ejemplo:

Antes de nada, como verá en la sección referencias, hay muchas formas de hacer lo mismo, pero desde mi punto de vista esta forma que he diseñado gana en sencillez, bajo acoplamiento y esfuerzo para transladarlo a vuestros proyectos.

Os dejo el código fuente del proyecto (proyecto Eclipse, Maven) para que realiceis vuestras pruebas.

Creamos una anotación para cachear el resultado de cualquier método de nuestras aplicaciones:

package com.autentia.tutoriales.spring.aop.aspectj.cache;

import java.lang.annotation.*;

/**
 * Anotación para métodos que realizen operaciones de cacheo de información
 * @author Carlos García. Autentia.
 */
@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.METHOD)
public @interface Cachea {
	/**
	 * @return Identificador dentro de la cache con la que se guardará la información
	 */
	public String cacheKey();
	
	/**
	 * @return Número de milisegundos que tendrá validez la cache
	 * (0=infinito la información se guarda en disco su cerramos la aplicación o cuando se necesita "paginar" en base a la configuración de la cache)    
	 */
	public int expireTime();
}

Creamos una anotación para limpiar la cache creada usando la anotación anterior:

package com.autentia.tutoriales.spring.aop.aspectj.cache;

import java.lang.annotation.*;

/**
 * Anotación para métodos que realizen operaciones de descacheo de información
 * @author Carlos García. Autentia.
 */
@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.METHOD)
public @interface Descachea {
	/**
	 * @return Identificador de la información cacheada a descachear
	 */	
	public String cacheKey();
}

Aspecto que automáticamente será invocado en los métodos que estén anotados para ser cacheados o descacheados:

package com.autentia.tutoriales.spring.aop.aspectj.cache;

import java.lang.reflect.Method;
import com.opensymphony.oscache.general.GeneralCacheAdministrator;
import com.opensymphony.oscache.web.filter.ExpiresRefreshPolicy;
import com.opensymphony.oscache.base.NeedsRefreshException;
import java.util.Properties;
import org.aspectj.lang.Signature;

/**
 * Aspecto que se encarga del sistema de cache de la aplicación
 * @author Carlos García. Autentia.
 * @see http://www.mobiletest.es
 */
@org.aspectj.lang.annotation.Aspect
public class CacheAspect {
	
	private final int	 NO_EXPIRE	 = 0;
	private final String CACHE_KEY	 = "cacheKey";
	private final String EXPIRE_TIME = "expireTime";
	
	private GeneralCacheAdministrator cache;
	
	/**
	 * Sí, si.. está acoplado a OSCache, podría haber definido una inferfaz que me desacoplara del sistema de cache... 
	 * Recuerda que esto es un tutorial y no tenia ganas.
	 * @param cache Sistema de cache nativo, no reinventemos la rueda :-) 
	 */
	public CacheAspect(com.opensymphony.oscache.general.GeneralCacheAdministrator cache){
		this.cache = cache;
	}
	
	/**
	 * Este método será llamado por AOP para aquellos métodos con la anotación Cachea 
	 */
	@org.aspectj.lang.annotation.Around("@annotation(Cachea)")
	public Object cachear(org.aspectj.lang.ProceedingJoinPoint call) throws Throwable {
		Properties cacheProps = this.getAnnotationProperties(call, true);
	    Object	 result		  = null;
	    String	 cacheKey	  = cacheProps.getProperty(CACHE_KEY);
	    int		 expire		  = NO_EXPIRE;

	    expire	 = Integer.parseInt(cacheProps.getProperty(EXPIRE_TIME));
    	// Evitamos números negativos como valor de tiempo de validez de la cache
    	if (expire < 0){
    		expire = NO_EXPIRE;
    	}
	    
	    try {  
	    	 result  = cache.getFromCache(cacheKey);
	    } catch (NeedsRefreshException e) {
	    	 // Los datos de la cache no existen o han caducado
	    	cache.cancelUpdate(cacheKey);  
	    }  	    
	   
	    if (result == null) {
	    	// Actualmente no hay datos cacheados validos, ejecutamos el método y 
	    	// cacheamos el resultado.  
	    	result = call.proceed();
	    	
	    	if (expire == 0){
	    		// No se especificó un tiempo de validez
	    		cache.putInCache(cacheKey, result);	
	    	} else {
	    		// Tiene un tiempo de validez
	    		cache.putInCache(cacheKey, result, new ExpiresRefreshPolicy(expire));
	    	}
		}
		
		return result;
	}
	
	/**
	 * Este método será llamado por AOP. 
	 * Vemos si el método tiene parámetros de descacheo a través de la anotación Descachea 
	 */	
	@org.aspectj.lang.annotation.Around("@annotation(Descachea)")
	public Object descachear(org.aspectj.lang.ProceedingJoinPoint call) throws Throwable {
		// Ejecutamos el método
		Object result	 = call.proceed();
		
		// Descacheamos
		Properties cacheProps = this.getAnnotationProperties(call, false);
		String	  cacheKey	  = cacheProps.getProperty(CACHE_KEY);		
		cache.removeEntry(cacheKey);

		// Devolvemos el resultado del método
		return result;
	}	

	
	/**
	 * @return Devuelve un properties con los atributos de la anotación (Cachea o Descachea)
	 */	
	private Properties getAnnotationProperties(org.aspectj.lang.ProceedingJoinPoint call, boolean isCacheo) {
		Properties	properties = new Properties();
		Method		metodo	   = this.getCallMethod(call);
		
		if (isCacheo){
			Cachea	anotacion  = metodo.getAnnotation(Cachea.class);
			properties.put(CACHE_KEY, anotacion.cacheKey());
			properties.put(EXPIRE_TIME, String.valueOf(anotacion.expireTime()));
		} else {
			Descachea anotacion = metodo.getAnnotation(Descachea.class);
			properties.put(CACHE_KEY, anotacion.cacheKey());
		}
		return properties;
	}

	
	/**
	 * @return Devuelve una referencia al método invocado por AOP
	 */
	@SuppressWarnings("unchecked")
	private Method getCallMethod(org.aspectj.lang.ProceedingJoinPoint call){
		Method metodo	  =  null;
		
		try {
			Signature sig     = call.getSignature();
			Class  clase	  = sig.getDeclaringType();
			String methodName = sig.getName();
			Object[] args	  = call.getArgs();
			Class[] params	  = new Class[args.length];
			
			for (int i = 0, count = args.length; i < count; i++){
				params[i] = args[i].getClass();
			}
			
			metodo	 = clase.getMethod(methodName, params);
		} catch (Exception e) {
			// Ignoramos: SecurityException, NoSuchMethodException
		}
		
		return metodo;
	}	
}

Archivo de configuración de Spring 2 (/main/resources/applicationContext.xml):

<?xml version="1.0" encoding="UTF-8"?>


	
	
	
	
	
		
			
			 
			
			    
			        
			        	
			            com.opensymphony.oscache.plugins.diskpersistence.DiskPersistenceListener
			            
			            
			            ${java.io.tmpdir}
			            
			            
			            false

			            
			            1000
			            
			            
			            com.opensymphony.oscache.base.algorithm.LRUCache			            
			        
			    
			
		
		
	
	
 	

Archivo de configuración de Maven pom.xml:


	4.0.0
	com.autentia.tutoriales
	spring_aop_cache_aspectj
	jar
	1.0-SNAPSHOT
	spring_aop_cache_aspectj
	https://adictosaltrabajo.com
	
		
			
			
				maven-compiler-plugin
				
					1.6
					1.6
					UTF-8
				
			
		
	
	
		
			org.springframework
			spring
			2.5.6
		
		
		
			org.springframework
			spring-core
			2.5.6
		
	
		
			org.springframework
			spring-aspects
			2.5.6
		
	
		
		
			cglib
			cglib-nodep
			2.2
		
		
		
		    opensymphony
		    oscache
		    2.4
		
				
		
			junit
			junit
			4.1
			test
		
	

Ejecuto un ejemplo a modo de test funcional:

Puede ser ejecutarlo con la sentencia: mvn test.

package com.autentia.tutoriales.spring.aop.aspectj.cache;

import org.junit.Assert;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.autentia.tutoriales.spring.aop.aspectj.Provincias;

/**
 * Lanzo el ejemplo a modo de test funcional..
 * ya se que esto no es un test valido real, sólo lo hago para ahorrarme tiempo 
 */
public class SpringCacheTest  {

	private ApplicationContext  factory;  

	/** 
	 * Inicializamos el contexto de Spring 
	 */  
	@org.junit.Before  
	public void initTests(){  
		this.factory  = new ClassPathXmlApplicationContext("applicationContext.xml");
	}  

	@org.junit.Test
	public void testCache() {
		Provincias provincias;
		java.util.List lista;
		long timestamp = 0;
		
		try {
			provincias = (Provincias) factory.getBean("provincias") ;

			System.out.println("Llamada add para forzar el descacheo");
			provincias.add("a");
			
			timestamp  = System.currentTimeMillis();			
			lista = provincias.getAll();
			System.out.println("Llamada getAll: " + (System.currentTimeMillis() - timestamp) + " milisegundos");
			
			timestamp  = System.currentTimeMillis();
			lista = provincias.getAll();
			System.out.println("Llamada getAll: " + (System.currentTimeMillis() - timestamp) + " milisegundos");

			System.out.println("Llamada add para forzar el descacheo");
			provincias.add("b");
			
			timestamp  = System.currentTimeMillis();
			lista = provincias.getAll();
			System.out.println("Llamada getAll: " + (System.currentTimeMillis() - timestamp) + " milisegundos");
			
			System.out.println("FIN");
			
			Assert.assertTrue(true);
		} catch (Exception ex){
			System.err.println(ex);
			Assert.fail();
		}
	}
}

Referencias

Conclusiones

Como habéis podido ver la programación orientada a aspectos deja nuestro código mucho más desacoplado, centrándonos en la lógica de negocio y dejando los temas como seguridad, gestión de trazas, cacheo, etc. al margen del mismo... en este tema Spring nos proporciona un amplísimo abanico de posibilidades.

En Autentia, estamos constantemente formándonos para intentar conseguir cada vez software de más calidad. Espero nos tengais en cuenta si necesitais algún tipo de consultaría o formación a medida.

Al margen de este tutorial, os invito a que profundizeis en esta importante
filosofía de desarrollo de sistemas pues como dije antes, esto no es más que un tutorial y no un libro concreto y/o especializado en programación orientada a aspectos, Spring, etc.

Carlos García Pérez. Creador de MobileTest, un complemento educativo para los profesores y sus alumnos.

cgpcosmad@gmail.com


2 COMENTARIOS

  1. Descubrimos q al utilizar Interfaces no funciona!
    lo resolvimos asi!! Saludos!

    @SuppressWarnings(\\\»unchecked\\\»)
    private Method getCallMethod(org.aspectj.lang.ProceedingJoinPoint call){
    Method metodo = null;

    try {
    Signature sig = call.getSignature();
    Class clase = sig.getDeclaringType();
    String methodName = sig.getName();
    Object[] args = call.getArgs();
    Class[] params = new Class[args.length];

    for (int i = 0, count = args.length; i < count; i++){
    params[i] = args[i].getClass();
    }

    metodo = clase.getMethod(methodName, params);

    if (metodo.getDeclaringClass().isInterface()) {
    metodo = call.getTarget().getClass().getDeclaredMethod(methodName, metodo.getParameterTypes());
    }

    } catch (Exception e) {
    // Ignoramos: SecurityException, NoSuchMethodException
    }

    return metodo;
    }

  2. Si no usais xml para la definición del aspecto (como en en este tutorial) teneis que ANOTAR el aspecto con @Component sino no funcionará.

    static.springsource.org/spring/docs/3.1.0.M2/spring-framework-reference/html/aop.html#aop-at-aspectj

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad