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
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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<String> provincias; /** * Constructor */ public Provincias(){ this.provincias = new java.util.ArrayList<String>(); } /** *(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<String> 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:
1 2 3 4 5 6 7 8 9 |
............ 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 |
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)
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd"> <!-- Bean de la lógiga de negocio de mi aplicación de ejemplo. --> <bean id="provincias" class="com.autentia.tutoriales.spring.aop.aspectj.Provincias"/> <!-- Aspecto que proporciona cacheo/descacheo de información del resto del sistema --> <bean id="cacheService" class="com.autentia.tutoriales.spring.aop.aspectj.cache.CacheAspect"> <constructor-arg index="0"> <!-- Le inyectamos el sistema de cache nativo a usar. --> <!-- Sí, si.. está acoplado a OSCache, podría haber definido una inferfaz que me desacoplara del sistema de cache... --> <bean class="com.opensymphony.oscache.general.GeneralCacheAdministrator"> <constructor-arg index="0"> <props> <!-- Habilitamos el cache en disco --> <prop key="cache.persistence.class">com.opensymphony.oscache.plugins.diskpersistence.DiskPersistenceListener</prop> <!-- Especificamos el directorio donde se cacheará la información --> <prop key="cache.path">{java.io.tmpdir}</prop> <!-- Indicamos que queremos un límite también para los elementos cacheados en disco --> <prop key="cache.unlimited.disk">false</prop> <!-- Como mucho serán cacheados 1000 objetos simultánemente --> <prop key="cache.capacity">1000</prop> <!-- Cuando se supere el límite de 1000 objetos, se aplicará el algoritmo LRU (Least Recently Used) para obtener espacio en disco y cachear los nuevos elementos. --> <prop key="cache.algorithm">com.opensymphony.oscache.base.algorithm.LRUCache</prop> </props> </constructor-arg> </bean> </constructor-arg> </bean> <!-- Configura el proxy para los aspectos definidos en nuestra aplicación. --> <aop:aspectj-autoproxy /> </beans> |
Archivo de configuración de Maven pom.xml
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.autentia.tutoriales</groupId> <artifactId>spring_aop_cache_aspectj</artifactId> <packaging>jar</packaging> <version>1.0-SNAPSHOT</version> <name>spring_aop_cache_aspectj</name> <url>http://www.adictosaltrabajo.com</url> <build> <plugins> <!-- Indicamos que use Java 6 --> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>2.5.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>2.5.6</version> </dependency> <!-- Cglib AOP Proxy, para Java > 1.4 --> <dependency> <groupId>cglib</groupId> <artifactId>cglib-nodep</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>opensymphony</groupId> <artifactId>oscache</artifactId> <version>2.4</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.1</version> <scope>test</scope> </dependency> </dependencies> </project> |
Ejecuto un ejemplo a modo de test funcional:
Puede ser ejecutarlo con la sentencia: mvn test
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
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<String> 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
- http://static.springframework.org/spring/docs/2.5.x/reference/aop.html
- http://sourceforge.net/projects/spring-cache
- http://www.javalobby.org/java/forums/t44746.html
- http://www.proactiva-calidad.com/java/spring/aop2.html
- http://www.oracle.com/technology/pub/articles/dev2arch/2006/05/declarative-caching2.html
- http://opensource.atlassian.com/confluence/spring/display/DISC/Caching+the+result+of+methods+using+Spring+and+EHCache
- http://opensource.atlassian.com/confluence/spring/display/DISC/AOP+Cache
- http://wanghy.sourceforge.net/cache/index.html
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
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;
}
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