Procesamiento de anotaciones y AOP

(Un título alterno podría ser "Mi primera experiencia implementando AOP con AspectJ")

Recientemente, durante mi conferencia de jAlarms en la primera edición de las OpenTalks de javaMéxico, me fue sugerido hacer algo con AOP para poder interceptar excepciones y enviar una alarma con jAlarms. La idea me pareció bastante interesante y la verdad en cuanto pude me puse a trabajar en el asunto.

Lo primero fue leer un poco acerca de AOP, sobre todo acerca de la implementación de aspectos, pues aunque conozco la teoría de cómo funciona e incluso he configurado algunos proxies, esta vez requiero un conocimiento más detallado del funcionamiento interno para poder implementar algo útil.

El objetivo final es muy sencillo: Crear una anotación que se pueda poner a un método o a una clase, y que en tiempo de ejecución si el método anotado (o cualquier método de la clase anotada) arroja una excepción que no maneja, se envíe una alarma con algunos detalles de la excepción. Algo así:

@AlarmOnException
public class UnaClase {

  public void metodo1() {
    //Una excepcion en este metodo causa envio de alarma
  }

  public void metodo2() {
    //Una excepcion en este metodo causa envio de alarma
  }
}

Incluso pensé en que se pueden detallar algunas cosas en la anotación, como por ejemplo el mensaje de alarma, especificar la fuente de la alarma, y el número de líneas del stack trace que se deben incluir en la alarma (0 para omitir el stack trace, -1 para incluirlo completo, o cualquier otro número para incluir máximo ese número de líneas):

public class OtraClase {

  public void metodo1() {
    //Una excepcion aqui no envia alarma
  }

  @AlarmOnException(message="Mensaje de la alarma",
    source="fuente", stack=3)
  public void metodo2() {
    //Una excepcion aqui causa envio de alarma
  }
}

Eso se ve muy conveniente de usar. Pero para poder usarlo, necesitamos tener algunas cosas tras bambalinas. A fin de cuentas, lo que se necesita es algo que detecte esa anotación para hacerle algo a la clase o método anotado, para que en caso de arrojarse una excepción, se intercepte y se envíe la alarma (y de todas maneras se arroje por supuesto, no queremos comernos las excepciones así nomás).

Bien podría ponerme a hacer un procesador de anotaciones incluso como para que se modifique el bytecode o algo así, trabajando en conjunto con el compilador; pero estaría reinventando el hilo negro. Lo que necesito ya existe y se llama programación orientada a aspectos, AOP por sus siglas en inglés. Y ya existe una muy buena implementación en Java, llamada AspectJ.

En AOP, lo que tengo que hacer es definir un aspecto, que es a fin de cuentas código, el cual debe ser invocado en cierto momento, y también tengo que definir un punto de corte, que es la definición del lugar donde quiero insertar mi aspecto.

Con AspectJ, el aspecto lo puedo definir simplemente haciendo una clase y poniéndole una anotación. Y el punto de corte lo defino anotando un método, y en la anotación puedo definir mediante notación especial de AspectJ, en dónde quiero detectar dicha anotación. Dado que el aspecto a fin de cuentas es un objeto y puedo hacer que uno de sus métodos se invoquen para interceptar la excepción de otro objeto cuando ocurra, puedo tener ahí mi referencia al AlarmSender. Voy a definir dos puntos de corte, porque quiero uno para cuando la anotación se encuentra en la clase y otro para cuando la anotación se encuentra en un método.

@Aspect
public class AlarmAspect {
  @Resource
  private AlarmSender alarmSender;

  @Pointcut("@within(com.solab.alarms.aop.AlarmOnException)"
  public void classPointcut() {}

  @Pointcut("@annotation(com.solab.alarms.aop.AlarmOnException)")
  public void methodPointcut() {}

}

El @within() del punto de corte para la clase es una notación especial de AspectJ que indica que quiero detectar la anotación especificada, en una clase. El @annotation() es similar pero indica que quiero detectar esa anotación en un método.

Hasta aquí, solamente he definido el aspecto, pero no hace nada. Necesito definir el código que quiero que se invoque, y definir además que quiero que se invoque cuando ocurre una excepción. Aquí hago un paréntesis: a mí solamente me interesa que se invoque mi código cuando ocurre una excepción, pero en AOP se pueden definir aspectos que se invocan en otras circunstancias, se llaman advice y hay de varios tipos:

  • Before Advice es el código que se ejecuta antes del método interceptado.
  • After Advice es el código que se ejecuta después de que corre el método interceptado, siempre y cuando la ejecución del mismo termine de manera normal, o sea que no se haya arrojado ninguna excepción
  • Around Advice es el código que se puede invocar primero antes del método interceptado y se da oportunidad ahí mismo a que se decida si continúa la ejecución o no, es decir, podría sustituir por completo la ejecución del método interceptado, o solamente complementarla ejecutando algo de código antes y luego otro tanto después. Una especie de mezcla del before y el after pero con la oportunidad de decidir en el before si se continúa con la ejecución del método interceptado o no.
  • Throws Advice es la que se ejecuta solamente cuando el método interceptado arroja una excepción que interrumpe su ejecución.
  • Finally Advice es la que se ejecuta al final de un método; similar al after pero ésta se ejecuta sin importar que haya ocurrido una excepción, es decir siempre se va a ejecutar y aquí hay que detectar si se terminó de ejecutar el método interceptado de manera normal o no.

Pues bien, la manera de definir el código que queremos ejecutar en el punto de corte, es simplemnte definir un método. Y hay que anotar el método con alguna anotación que indica el tipo de advice que queremos. En mi caso, throws advice.

@AfterThrowing(pointcut="com.solab.alarms.aop.AlarmAspect.methodPointcut()")
public void sendMethodAlarm() {}

Eso no me va a ser muy útil porque no tengo manera de acceder a la información de la excepción. Pero la anotación @AfterThrowing me permite indicar el nombre del parámetro en mi método donde quiero recibir la excepción. Y además, dado que definí algunas propiedades en mi anotación, sería muy útil recibirla como parámetro en ese método. Y también quiero definir el advice para la clase.

@AfterThrowing(pointcut="com.solab.alarms.aop.AlarmAspect.methodPointcut() && @annotation(alarmOnException)",
  throwing="ex")
public void sendMethodAlarm(RuntimeException ex, AlarmOnException alarmOnException) {
  //Aqui voy a leer las propiedades de la anotación para saber cómo crear
  //mi mensaje de alarma, y voy a leer la clase y mensaje de la excepción
  //y opcionalmente su stacktrace parcial o total para incluirlo en la alarma
  //y finalmente enviaré la alarma.
}

@AfterThrowing(pointcut="com.solab.alarms.aop.AlarmAspect.classPointcut() && @within(alarmOnException)",
  throwing="ex"
public void sendClassAlarm(RuntimeException ex, AlarmOnException alarmOnException) {
  sendMethodAlarm(ex, alarmOnException);
}

OK, entonces ya tengo mi aspecto bien definido. Ahora lo interesante: ¿cómo lo uso?

La manera más fácil, sobre todo si ya están usando Spring en su aplicación, es usar Spring AOP. Spring AOP usa debajo del cofre la implementación de AOP Alliance (una implementación de AOP) y también la de AspectJ. Si se fijan hasta ahora no había tenido dependencia de Spring, de modo que solamente lo usaré en la configuración final; bien podría durante todo el desarrollo usar la anotación @AlarmOnException en código que no sabe nada de Spring y al final lo voy a unir todo solamente con application contexts y demás.

Spring AOP lo que hace es que en conjunto con AspectJ, detecta los beans que maneja en su application context, que tengan la anotación que hice, y crea proxies para dichos beans. Los proxies que crea dependen de algunas condiciones. Si los beans implementan una o varias interfaces y la anotación está en la clase o en los métodos de dichas interfaces, entonces se puede crear un proxy dinámico de JDK. Pero si anotan una clase que no implementa ninguna interfaz, o implementan un método protegido o que no está declarado en una interfaz, entonces necesitan generar un proxy con CGLIB. Para ello necesitan agregar la librería cglib-nodep o pueden agregar cglib y las dependencias requeridas por cglib, si es que ya tienen algunas y no quieren generar algún conflicto de versiones.

Supongamos que tienen una clase que implementa una interfaz, y solamente anotan un método de dicha interfaz. Entonces se puede crear un proxy dinámico y no requieren dependencias adicionales (aparte de las de Spring, AspectJ y, por alguna razón, AOP Alliance).

public class MiEjemplo implements InterfazImaginaria {
  @AlarmOnException(message="ejemplo", stack=5)
  public void metodoDefinidoEnInterfaz() {
    new java.math.BigInteger("invalido");
  }
}

Y luego en el application context de Spring, deben estar usando XSD y agregar el esquema de AOP, agregar una declaración para configurar auto-proxies y definir el aspecto (supongamos que ya tienen declarado el bean del AlarmSender y que se llama "alarmSender").

<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:aop="http://www.springframework.org/schema/aop"
  xsi:schemaLocation="http://www.springframework.org/schema/beans <a href="http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
" title="http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
">http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
</a>    <a href="
http://www.springframework.org/schema/context" title="http://www.springframework.org/schema/context">http://www.springframework.org/schema/context</a> <a href="http://www.springframework.org/schema/context/spring-context-3.0.xsd
" title="http://www.springframework.org/schema/context/spring-context-3.0.xsd
">http://www.springframework.org/schema/context/spring-context-3.0.xsd
</a>    <a href="
http://www.springframework.org/schema/aop" title="http://www.springframework.org/schema/aop">http://www.springframework.org/schema/aop</a> <a href="http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

<aop:aspectj-autoproxy" title="http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

<aop:aspectj-autoproxy">http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

<aop:asp...</a> />
<context:annotation-config />

<bean id="alarmAspect" class="com.solab.alarms.aop.AlarmAspect">
  <property name="message" value="Algo malo ha ocurrido" />
  <!-- La propiedad alarmSender se auto-conecta -->
</bean>

</beans>

Ahora, cuando un componente invoca el método metodoDefinidoEnInterfaz() de nuestro componente que está ya anotado, se envía una alarma con un texto similar a este (usando el TestChannel, esto se vería en consola):

ALARM: ejemplo java.lang.NumberFormatException: For input string: "invalido"
java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
java.lang.Integer.parseInt(Integer.java:449)
java.math.BigInteger.(BigInteger.java:316)
java.math.BigInteger.(BigInteger.java:451)
com.solab.example.MiEjemplo.metodoDefinidoEnInterfaz(MiEjemplo.java:4)

Se incluyen solamente 5 líneas del stack trace en la alarma porque así se definió en la anotación.

Todo esto es solamente una manera más de usar jAlarms, esto no significa que esta biblioteca sirve sólo para enviar mails cuando ocurre una excepción; es muy útil para enviar mensajes cuando ocurren condiciones en un sistema que requieren intervención humana, pero este ejemplo que indico aquí es útil para cuando se están probando sistemas que siguen en desarrollo donde es susceptible que ocurran excepciones que no deberían ocurrir (bueno, pero ¿cuándo ocurren excepciones que deberían ocurrir?)

Espero que esto además sirva como ejemplo de uso de AspectJ y de AOP en general.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de VictorManuel

:)

Gracias por este tuto me servira para un proyectito de un DDBMS

Excelente tuto

Estoy estudiando AOP para familiarizarme con Spring...buena info

Imagen de neko069

Gracias....

Estoy igual que @carraro, estudiando aspectos, y bueno, sólo porque me estás insistiendo ; ) usaré tu jAlarms, para un proyecto personal, que, precisamente estaba ideando cómo hacer para que, al momento de requerir notificaciones, pues se envíe un correo con información específica.... y mira, me encuentro con tu blog post, gracias de nuevo!!!

Imagen de ezamudio

no es para eso

neko069, jAlarms no es para envio de notificaciones personales. Es decir no es para enviar un correo a un nuevo usuario para activarse ni nada de eso. Es para enviar alarmas a los administradores/desarrolladores/encargados de un sistema, cuando el sistema tiene algun error o situación que requiere de intervención humana (por ejemplo, se cae la base de datos, o no se puede realizar alguna acción que se debería de poder y la causa es que algo está mal configurado, etc).

Si eso es lo que necesitas, jAlarms es tu solución, pero si lo que quieres es enviar correos a usuarios, jAlarms no está hecho para eso.

Imagen de neko069

Precisamente....

Mea culpa, me expliqué mal .... eso de ayunar no deja nada bueno....
Si voy a usar tú librería como lo describes, para envío de alarmas, en este caso al administrador (o sease mí mismo...) y por medio de configuración de aspectos, pues queda muy limpio el código, y principalmente es, como lo mencionas, en caso de que pase algo que no debería de pasar en situaciones normales....