Spring AOP, conceptos y ejemplos en la capa transversal de las arquitecturas

El desarrollo de aplicaciones empresariales requiere de aplicar prácticas adecuadas que ayuden a eliminar la complejidad accidental que como consecuencia del crecimiento y los constantes cambios se va introduciendo en las aplicaciones.

Los patrones permiten modelar software robusto basado en arquitecturas adecuadas a cada escenario posible de un negocio. Muchas de las arquitecturas y/o estilos arquitectónicos contemplan elementos transversales, comunes a todo el sistema. Principalmente, la capa “cross cutting” se encarga de elementos tales como logging, cache, auditoria, entre otros. En este capa, especialmente la aplicación del paradigma orientado a aspectos es útil.

Programación orientada a aspectos

Del blog de codingornot, tomamos la siguiente definción: la programación orientada a aspectos (POA) es un paradigma de programación que basa su filosofía en tratar las obligaciones transversales de nuestros programas como módulos separados (aspectos) para lograr una correcta separación de responsabilidades. Una obligación transversal es aquella que se repite en varias partes de un programa independientemente de si las secciones en las que aparece tienen relación directa.

Conceptos principales de la AOP

En la programación orientada aspectos existen 3 conceptos fundamentales.

Aspecto: El concepto de aspecto es el elemento fundamental del paradigma orientado a aspectos, se emplea para definir una funcionalidad que será transversal al sistema. Es una sección del software, separada que tiene la capacidad de impactar sobre otros elementos sin siquiera estar presente en ellos necesariamente.

Punto de corte: Es el que permite definir mediante expresiones regulares o anotaciones los lugares del código donde se inserta el aspecto (antes, después o alrededor [al inicio de ejecución y al final]).

Consejo o Advice: Es el código que ejecutará el aspecto (la implementación del aspecto en sí). Los Advice mas usados son: @Before, @AfterReturning, @AfterThrowing, @After y @Around.

Ventajas de AOP

  • Permite introducir funcionalidad sin necesidad de “tocar” código existente.
  • Permite desarrollar software con una arquitectura más limpia.
  • Permite agilizar el proceso de creación de programas cuando muchas personas están involucradas en el mismo proyecto.
  • Permite combinarse con otros paradigmas de programación.

Desventajas de AOP

No todo en AOP son ventajas, el empleo de este paradigma involucra dos desventajas esenciales:

  • Dificulta la comprensión del funcionamiento del software y su legibilidad.
  • Implementa un anti-patrón: Acciones a distancia.

Ejemplos prácticos con Spring AOP

Spring AOP es el componente dentro Spring de dar soporte a aspectos de forma sencilla de cara al usuario dentro del framework.

En los siguientes ejemplos probaremos algunos advice, enunciando un caso de uso donde podemos utilizarlos en la práctica de una aplicación. También veremos ejemplos de usar AOP mediante anotaciones, crearemos anotaciones para ejemplificar la potencia de los aspectos. Al final el código fuente estará disponible para descargar.

Estructura general de un aspecto en Java con Spring AOP

La estructura para crear un aspecto con Spring AOP es la siguiente:

@Aspect
@Component
public class ExecutionTime {
    @Around("execution(* com.paquete.*.*(*))")
    public void getServicesExecutionTime(ProceedingJoinPoint point) {
            point.proceed();
    }
}

La anotación @Aspect indica que vamos a crear un aspecto, @Around define el advice que estamos usando, cualquiera de los advices es posible, el método getServicesExecutionTime(ProceedingJoinPoint point) define la implementación del aspecto [ la acción que va a realizar ] , en este particular se le pasa al método el JoinPoint, que indica el “espacio de ejecución” de la función que matchea con la expresión regular definida en el advice {com.paquete.*.(*))”}, esto indica que el aspecto se aplicará alrededor de la ejecución de cualquier método del paquete com.paquete.[cualquier-paquete].[cualquier-clase][cualquier-método][el método posee o no parámetros], en esencia todas las funciones dentro de com.paquete serán sometidas a la implementación del aspecto.

Ejemplo 1: Midiendo el tiempo de ejecución de los métodos.

@Aspect
@Component
public class ExecutionTime {
    @Around("execution(* com.yoandypv.aspectos.auditoria.service.*.*(*))")
    public void getServicesExecutionTime(ProceedingJoinPoint point) {
        try {
            Long tiempo1 = System.currentTimeMillis();
            point.proceed();
            Long tiempo2 = System.currentTimeMillis();
            Long total = tiempo2 - tiempo1;
            System.out.println("[Without annotation] The time execution is: " + total);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

Ejemplo 2: Implementando un interceptor de errores que se generan en la aplicación.

@Aspect
@Component
public class ErrorInterceptor {
    @AfterThrowing(pointcut = "execution(* com.yoandypv.aspectos.auditoria.*.* (..))", throwing = "ex")
    public void errorInterceptor(Exception ex) {
         System.out.println(ex.getMessage());
    }
}

Ejemplo 3: Implementando un medidor de tiempo de ejecución de los métodos, pero basado en anotaciones.

En este caso particular, y en todos los que aspectos se aplica a través de anotaciones, el primer paso es crear una anotación para ello. La anotación que crearemos será aplicable sobre métodos. Llamemos a la anotación GrabExecutionTime, y se encargará de medir el tiempo de ejecución de los métodos anotados.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GrabExecutionTime {
}

Ahora implementaremos el aspecto que hace uso de la anotación.

@Aspect
@Component
public class ExecutionTimeAnnotation {
    @Around("@annotation(com.yoandypv.aspectos.auditoria.aop.annotation.GrabExecutionTime)")
    public void grabExecutionTime(ProceedingJoinPoint point) {
        try {
            Long tiempo1 = System.currentTimeMillis();
            point.proceed();
            Long tiempo2 = System.currentTimeMillis();
            Long total = tiempo2 - tiempo1;
            System.out.println("[With annotation]The time execution is: " + total);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

Es importante colocar el path completo a la anotación que hemos creado.

Ejemplo 4: Implementando un sistema de auditoria de acciones basado en anotaciones.

En este caso crearemos una anotación, para cada vez que anotemos un método con ella podamos saber la acción que se realizó y el usuario que lo hizo, una herramienta muy poderosa para aplicar auditoria sobre nuestras aplicaciones sin tocar el código del negocio de la aplicación.

Primero creemos la anotación, en este caso para variar añadiremos parámetros a la anotación, el cual indicará la acción que ejecuta el método que anota.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Watcher {
    String action() default "";
}

Implementando el aspecto:

@Aspect
@Component
public class WatchUserActions {
    @After( "@annotation(com.yoandypv.aspectos.auditoria.aop.annotation.Watcher)")
    public void watchUserActions(JoinPoint point) {
        try {
            // Obtenemos la accion colocada en la anotacion
            MethodSignature ms = (MethodSignature) point.getSignature();
            Method method = ms.getMethod();
            Watcher watcher = method.getAnnotation(Watcher.class);
            String action = watcher.action();
            // Obtenemos los parametros del metodo
            Object [] methodArgs = point.getArgs();
            System.out.print("Parametros: ");
            for(Object o: methodArgs)
                System.out.print(o.toString());
            System.out.println("");
            // Podemos agregar el usuario que hizo la accion (Si tuvieramos el usuario en el contexto )
            //Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            //String name = auth.getName(); //get logged in username
            String name = "userlogged";
            // Fecha
            Date date = new Date();
            String msg = String.format("El usuario %s realizó la acción %s pasando %s arguments las %s", name, action, methodArgs.length, date);
            System.out.println(msg);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

No añadiremos mucho más, los comentarios en el código explican el funcionamiento del aspecto, solo notar que en este caso usamos el advice After.

Evidentemente en este caso estamos en todos los casos imprimiendo el resultado, pero bien pudiéramos grabar a bases de datos el resultado o mejor aún enviarlo un bus de mensajes si deseamos. Por cierto acá puedes ver como montar un broker pub/sub que puede ser útil.

Conclusiones

Los beneficios de AOP pueden ser aprovechados para disminuir el acoplamiento de nuestras aplicaciones y dotarlos de funcionalidades que en realidad aportan mucho valor al negocio.

Espero te guste al artículo, deja algún comentario y/o compártelo. Puedes acceder al código fuente acá.

7 thoughts on “Spring AOP, conceptos y ejemplos en la capa transversal de las arquitecturas

  1. Muy buen artículo. AOP está para ayudarnos siempre y cuando tengamos una clara diferenciación entre lo que es transversal o no en nuestro sistema. Lo que nos acerca más al anti-patrón es pensar que acciones “indirectas” son acciones transversales. Lo ejemplos que muestras son claramente acciones transversales que explotan todas las ventajas de AOP. El problema está cuando empezamos a pensar que acciones de cierta lógica que deben lanzar lógicas de negocio diferentes sería buena idea implementar también AOP.

  2. Hey there. I found your website by the use of Google whilst looking for a comparable subject, your site came up. It seems great. I’ve bookmarked it in my google bookmarks to visit then.

Comments are closed.