AOP con AspectJ y Maven

1
19032

AOP con AspectJ y Maven

Índice de contenido

Introducción.

La programación orientada a aspectos es algo que cada vez está más presente en varios proyectos, y que gracias a Spring AOP se integra perfectamente en el framework de Spring como podemos ver en el tutorial Spring AOP: Cacheando aplicaciones usando anotaciones y aspectos con Aspectj de nuestro compañero Carlos. Pero no en todos los proyectos contamos con Spring, pero no por esto tenemos que descartar la programación de aspectos si es que nos hacen falta.

En este tutorial vamos a ver un ejemplo de programación de aspectos con AspectJ y como podemos integrarla dentro de nuestros proyectos de Maven mediante un plugin de compilación, sin necesidad de integrarlo con el framework de Spring.

Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portatil Samsung R70 ( Intel(R) Core(TM)2 Duo 2,5Ghz, 2046 MB RAM, 232 Gb HD)
  • Sistema Operativo: Windows Vista Home Premium
  • Máquina Virtual Java: JDK 1.5.0_14 de Sun Microsystems (http://java.sun.com/javase/downloads/index_jdk5.jsp
  • IDE Eclipse 3.4 (Ganymede) (http://www.eclipse.org/downloads/)

AOP (Aspect Oriented Programming).

En este punto vamos a presentar los conceptos básicos de AOP que sirvan de base para alguién nuevo o de refresco para aquellos que lo conocen.

De forma general podemos decir que la programación orientada a aspectos trata de identificar y ocuparse de aquellos comportamientos que afectan de forma transversal a nuestra lógica de negocio. Tipicamente son del tipo:

  • Comprobaciones de seguridad (verificación de credenciales).
  • Gestión de transacciones (apetura y cierre)
  • Volcado de trazas.
  • etc.

Con AOP podemos implementar este tipo de comportamientos sin necesidad de modificar el código propio de la lógica de negocio.

Para terminar de asentar las bases de AOP vamos a ver un poco de su terminología principal:

  • Aspect: Es el comportamiento transversal que se aplica sobre aquella funcionalidad configurada bajo el aspecto. (verificación de credenciales, transacciones, trazas, etc.)
  • Join point: Es cualquier punto de la aplicación sobre el que se puede aplicar el aspecto. (Llamadas a métodos, acceso a atributos, etc.)
  • Advice: Es la acción que realiza un aspecto en un determinado join point. (volcado de trazas, etc.)
  • Pointcut: Es un conjunto de join points al que aplicar un advice. (ej. Todos lo métodos «setXXX» de una clase)

Integración de AspectJ con Maven.

Como hemos avanzado en la introducción, al no tener Spring debemos encontrar un modo de que nuestros aspectos se «enganchen» correctamente a nuestra aplicación, y se ejecuten en los casos necesarios.

AspectJ consigue realizar esto al compilar nuestro proyecto con un compilador propio. Podemos decir que al compilar una clase afectada por algún aspecto, el compilador de AspectJ modifica el código introduciendo las llamadas al aspecto.

Al tener nuestro proyecto gestionado con Maven, es necesario un plugin de Maven que en el momento de compilar realice estas modificaciones. El plugin en concreto es AspectJ compiler Maven Plugin. También deberemos tener incluidas las dependencias propias de AspectJ, por lo que en nuestro fichero «pom.xml» deberemo incluir el siguiente código:

<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">
	.....
	<build>
  	<plugins>
  	   .....
	  	<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>aspectj-maven-plugin</artifactId>
				<version>1.1</version>
				<configuration>
					<source>1.5</source>
				</configuration>
				<executions>
					<execution>
						<goals>
							<goal>compile</goal>       <!-- use this goal to weave all your main classes -->
							<goal>test-compile</goal>  <!-- use this goal to weave all your test classes -->
						</goals>
					</execution>
				</executions>
			</plugin>
			.....
  	</plugins>
  </build>
	.....
	<dependencies>
	   .....
	   <!-- AspectJ -->
	   <dependency>
			 <groupId>org.aspectj</groupId>
			 <artifactId>aspectjrt</artifactId>
			 <version>1.6.2</version>
		 </dependency>
		 .....
	</dependencies>
	.....
</project>

 

De la configuración de este plugin, lo más destacable es que debe ejecutarse en el momento de compilacion tal y como podemos ver en sus «goals» específicos. Pero otro dato importante es su configuración con el elemnento «<source>» con valor «1.5»; que nos sirve para indicar que el nivel de compilación se corresponde con Java 1.5, de esta forma podemos utilizar anotaciones para nuestros aspectos, algo que nos simplifica bastante la configuración de los mismos.

Ejemplo.

Para ver rápidamente como podemos crear aspectos, vamos a implementar un sistema de trazas con aspectos. Tenemos el típico CRUD (Create, Read, Update, Delete) de contactos, donde deseamos que cada vez que se inserte, modifique o borre un contacto se deje una traza con el tipo de acceso.

Primero nos creamos nuestras clases de trazas.

package com.autentia.tutoriales.aspectj.businessobject;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Lob;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Trace implements Serializable {

	private static final long serialVersionUID = -3089626883233965972L;

	private Integer id;

	private Date whenDate = new Date();

	private String description;
	
	@SuppressWarnings("unused")
	private Type type;
	

	@Id
	@GeneratedValue
	public Integer getId() {
		return id;
	}

	@SuppressWarnings("unused")
	private void setId(Integer id) {
		this.id = id;
	}

	@Lob
	@Column(nullable = false)
	public String getDescription() {
		return description;
	}

	@Column(nullable = false)
	@Temporal(TemporalType.TIMESTAMP)
	public Date getWhenDate() {
		return whenDate;
	}

	@SuppressWarnings("unused")
	private void setWhenDate(Date whenDate) {
		this.whenDate = whenDate;
	}

	public void setDescription(String description) {
		this.description = description;
	}


	@Override
	public int hashCode() {
		final HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
		hashCodeBuilder.append(getWhenDate());
		hashCodeBuilder.append(getDescription());
		return hashCodeBuilder.toHashCode();
	}

	public boolean equals(Object obj) {
		boolean isEquals = false;
		try {
			if (this == obj)
				return true;
			final Trace other = (Trace) obj;
			final EqualsBuilder eqb = new EqualsBuilder();
			eqb.append(getWhenDate(), other.getWhenDate());
			eqb.append(getDescription(), other.getDescription());
			isEquals = eqb.isEquals();
		} catch (Exception e) {
			isEquals = false;
		}
		return isEquals;
	}
	
	public abstract Type getType();

	@SuppressWarnings("unused")
	private void setType(Type type) {
		this.type = type;
	}

	public enum Type{INSERT, UPDATE, DELETE}
}

 

package com.autentia.tutoriales.aspectj.businessobject;

import javax.persistence.Entity;

@Entity
public class InsertTrace extends Trace {

	/**
	 * 
	 */
	private static final long serialVersionUID = 6876829701547179312L;

	@Override
	public Type getType() {
		return Type.INSERT;
	}	
	   
}
package com.autentia.tutoriales.aspectj.businessobject;

import javax.persistence.Entity;

/**
 * 
 *
 */
@Entity
public class UpdateTrace extends Trace {

	
	private static final long serialVersionUID = 47615098436209786L;

	@Override
	public Type getType() {
		return Type.UPDATE;
	}	
	   
}

 

package com.autentia.tutoriales.aspectj.businessobject;

import javax.persistence.Entity;

/**
 * 
 *
 */
@Entity
public class DeleteTrace extends Trace {

	
	private static final long serialVersionUID = 47615098436209786L;

	@Override
	public Type getType() {
		return Type.DELETE;
	}	
	   
}

 

 

Ahora debemos crearnos el DAO de acceso a datos. (Con ver el interfaz el suficiente para nuestro ejemplo, ahora veréis)

 

package com.autentia.tutoriales.aspectj.dao;

import java.util.List;

import com.autentia.tutoriales.aspectj.TraceAnnotation;
import com.autentia.tutoriales.aspectj.businessobject.Trace.Type;

public interface Dao {
	
	@TraceAnnotation(type=Type.DELETE)
	void delete(Object entity);
	
	<T> List<T> loadAll(Class<T> entityClass);
	
	@TraceAnnotation(type=Type.INSERT)
	void insert(Object entity);
	
	@TraceAnnotation(type=Type.UPDATE)
	<T> T update(T entity);
	
	
}

 

El punto interesante, se encuentra en que los métodos «delete», «insert» y «update» se encuentran anotado bajo la anotación «@TraceAnnotation». Esta anotación es la que nos sirve para indicar que éstos métodos serán «join points» de nuestro aspecto.

La anotación la hemos creado para marcar los métodos bajo el aspecto que vamos a crear, por lo que hemos creado una anotación específica para métodos y su código es el siguiente:

package com.autentia.tutoriales.aspectj;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

import com.autentia.tutoriales.aspectj.businessobject.Trace.Type;

@Retention(RetentionPolicy.RUNTIME)   
@Target(ElementType.METHOD) 
public @interface TraceAnnotation {
	public Type type() default Type.INSERT;
	
}

 

Finalmente nos queda por crear nuestro aspecto, que será el responsable de registrar los distintos tipos de trazas. Su código es:

package com.autentia.tutoriales.aspectj;

import java.lang.reflect.Method;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import com.autentia.tutoriales.aspectj.businessobject.DeleteTrace;
import com.autentia.tutoriales.aspectj.businessobject.InsertTrace;
import com.autentia.tutoriales.aspectj.businessobject.Trace;
import com.autentia.tutoriales.aspectj.businessobject.UpdateTrace;
import com.autentia.tutoriales.aspectj.businessobject.Trace.Type;
import com.autentia.tutoriales.aspectj.dao.Dao;
import com.autentia.tutoriales.aspectj.dao.DaoFactory;

@Aspect
public class TraceAspect {

	/** Logger */
	private static final Log log = LogFactory.getLog(TraceAspect.class);

	private final static String TYPE_KEY = "type";

	@After("@annotation(com.autentia.tutoriales.aspectj.TraceAnnotation)")
	public void addTrace(JoinPoint call) {
		try {
			Object entity = call.getArgs()[0];
			if (!(entity instanceof Trace)) {
				Type type = this.getAnnotationType(call);
				Trace trace = null;
				if (type.equals(Type.INSERT)) {
					trace = new InsertTrace();
				} else if (type.equals(Type.UPDATE)) {
					trace = new UpdateTrace();
				} else if (type.equals(Type.DELETE)) {
					trace = new DeleteTrace();
				}
				if (trace != null) {
					trace.setDescription(entity.toString());
					Dao dao = DaoFactory.getDao();
					dao.insert(trace);
				}
			}
		} catch (SecurityException e) {
			log.error(e);
		} catch (NoSuchMethodException e) {
			log.error(e);
		}
	}



	/**
	 * @return Devuelve el tipo de la anotación
	 * @throws NoSuchMethodException
	 * @throws SecurityException
	 * @throws ClassNotFoundException
	 */
	private Type getAnnotationType(JoinPoint call) throws SecurityException,
			NoSuchMethodException {
		Method metodo = this.getCallMethod(call);

		TraceAnnotation anotacion = metodo.getAnnotation(TraceAnnotation.class);
		return anotacion.type();

	}

	/**
	 * Returns the intercepted method
	 * 
	 * @param call
	 * @return
	 */
	private Method getCallMethod(JoinPoint call) {
		Method metodo = null;
		MethodSignature sig = (MethodSignature) call.getSignature();
		metodo = sig.getMethod();

		return metodo;
	}
}

 

Aquí lo más importante es ver como la clase está anotada bajo la anotación «@Aspect», que es una anotación propia de AspectJ, y que sirve para indicarle al compilador de AspectJ que esta clase es un aspecto.

Otro punto a destacar es el método «addTrace», que es el advice del aspecto (comportamiento a ejecutar). Se puede observar cómo este método está anotado con la anotación «@After» que indica que debe ejecutarse después de la llamada a los métodos anotados con «@TraceAnnotation».

Otras posibles opciones sería «@Before», para indicar que se ejecute antes de la llamada al método y «@Around» para indicar que debe ejecutarse tanto antes de la llamada al método como después.

Si observamos el código podemos ver como lo primero que hacemos es comprobar que la entidad sobre la que se está realizando la operación (insert, update o delete) no es de tipo «Trace» ya que ésta no interesan. Después recuperamos el tipo de operación, creamos la traza correspondiente y la guardamos en la base de datos.

Probando el ejemplo

Para poder probar nuestro aspecto nos creamos un test unitario que inserte, modifique y borre un contacto y depués de cada acción comprobaremos que se ha insertado la traza. El código del test es:

package com.autentia.tutoriales.aspectj.test;

import junit.framework.Assert;

import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import com.autentia.tutoriales.aspectj.businessobject.Contact;
import com.autentia.tutoriales.aspectj.businessobject.Trace;
import com.autentia.tutoriales.aspectj.dao.Dao;
import com.autentia.tutoriales.aspectj.dao.DaoFactory;

public class DaoImplTest {
	
	private static Dao dao;
	
	@BeforeClass
	public static void init() {

		DaoFactory.init();		
		dao = DaoFactory.getDao();
		
		
		DaoFactory.openSession();
		
	}

	@AfterClass
	public static void end() {
		DaoFactory.closeSession();
		DaoFactory.close(); 
	}
	
	@Test
	public void myTest() {
				
		Contact contact = new Contact();
		contact.setName("Borja");
		contact.setSurname("Lázaro");
		contact.setEmail("blazaro@autentia.com");
		 	
		dao.insert(contact);		
		
		Assert.assertEquals(1, dao.loadAll(Contact.class).size());
		Assert.assertEquals(1, dao.loadAll(Trace.class).size());		
		
		contact.setName("Borja modificado");
		
		dao.update(contact);
		
		Assert.assertEquals(1, dao.loadAll(Contact.class).size());
		Assert.assertEquals(2, dao.loadAll(Trace.class).size());	
		
		dao.delete(contact);
		
		Assert.assertEquals(0, dao.loadAll(Contact.class).size());
		Assert.assertEquals(3, dao.loadAll(Trace.class).size());	
		
	}
}

 

Conclusiones

Finalmente podemos ver cómo con AOP podemos implementar aquella funcionalidad que es transversal al resto de la aplicación, sin neecsidad de tener que cambiar el código propio de la lógica de negocio. También se puede observar como se consigue una mayor independencia de este tipo de tareas respecto al resto de la aplicación, el famoso bajo acoplamiento de las aplicaciones. Por ejemplo, si sobre el caso mostrado se quisiera cambiar el sistema de trazas a otro distinto, sólo habría que cambiar la implementación del aspecto, dejando el resto del código de nuestra aplicación sin tocar.

Si queréis, aquí podéis conseguir todo el código fuente de este ejemplo AOP con AspectJ y Maven.

Un saludo.
Borja Lázaro de Rafael.

1 COMENTARIO

  1. Muy buen tutorial. Lástima que no me funcione 🙁 . He importado el proyecto Maven, he incluido en el pom.xml la dependencia a «commons-collections» que hacía falta, y he lanzado los test desde maven. Me da error este assert:

    Assert.assertEquals(1, dao.loadAll(Trace.class).size());

    …porque no entra en el aspecto, por lo que el número de trazas es 0 en vez de 1, que es lo que espera el test tras invocar al insert.

    ¿Sabes a qué se puede deber que no se evalue el aspecto? ¿Hay que lanzar los test de algún modo especial para que funcione el «weave»?

    Muchas gracias y un saludo 😉

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