viernes, abril 06, 2007

Instrumentando clases Java con anotaciones y class weaving

viernes, abril 06, 2007 por Martín

Hace unos meses estuve experimentando un poco con anotaciones y he decidido a escribir aquí el resultado, como referencia para que no se me olvide, y por si de paso a alguien le interesa, así que esta entrada va a ser bastante técnica. Empiezo...

La monitorización de sistemas es algo realmente fundamental, al menos para aplicaciones de medio y gran tamaño. Hoy en día, sin un sistema que nos permita examinar en tiempo real el estado de nuestras aplicaciones, se hace realmente complicado el encontrar errores, especialmente en sistemas distribuidos. Por poner un ejemplo, si tenemos un sistema de caché que funciona en cluster puede ser muy importante el saber en cada momento cual es el contenido de la caché, el saber si unas cachés están más llenas que otras, el ser capaz de ejecutar operaciones sobre la caché, etc.

En Java, JMX (en .NET ¿WMI?) permite la instrumentación de sistemas y la obtención de datos en tiempo real a través de herramientas como jconsole. Un libro que ya he recomendado en algún otro post, Pro Java EE 5 Performance Management and Optimization tiene varios capítulos donde recalca la importancia de instrumentar nuestro código en sistemas distribuidos complejos y pone algunos ejemplos con JMX.

La primera opción que nos puede venir a la cabeza a la hora de instrumentar nuestro código es simplemente crear nuestros managed beans y añadir el código de instrumentación directamente a nuestros proyectos:
MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); 
ObjectName name = new ObjectName("com.example.mbeans:type=Hello"); 
Hello mbean = new Hello(); 
mbs.registerMBean(mbean, name); 


Esta opción es sostenible y perfectamente justificable para proyectos pequeños. En cuanto los proyectos son de mayor calibre e involucran una cantidad considerable de componentes y desarrolladores es mejor buscar una solución más mantenible.

La alternativa natural sería crear un framework de monitorización que centralize toda instrumentación del proyecto. Esta es una opción muy recomendable que puede perfectamente cubrir todas nuestras necesidades.

Una vez que tenemos creado nuestro framework de monitorización, lo único que tenemos que hacer es decirle a nuestros desarrolladores como utilizarlo, y listo. ¿En serio? Pues la verdad es que depende mucho de nuestros desarrolladores, del movimiento que haya en la empresa (¿están continuamente saliendo y entrando nuevos programadores?), y de nuestra capacidad de formar a los miembros más junior del equipo. Porque aunque el framework de monitorización nos ahorrará mucho trabajo, lo cierto es que si nuestros programadores no saben como utilizarlo, entonces a la larga puede ser una solución menos productiva. Por ejemplo nos podemos encontrar con que un desarrollador ha colocado objetos gigantescos en memoria, que otro ha utilizado el framework para exponer operaciones que exponen nuestro sistema, que otra persona ha ejecutado código muy costoso dentro de los managed beans, o en resumen que cada persona utiliza el framework de una manera diferente.

Una solución a este problema es hacer mucho más fácil y estándar el uso de nuestro framework de monitorización, y ahí es donde entra en juego el concepto de las anotaciones. Si añadimos anotaciones al framework, los desarrolladores sólo tendrán por ejemplo que etiquetar sus clases, métodos o atributos, y nuestro framework de monitorización realizará todo el trabajo sucio: crear los managed beans, gestionar su registro en la plataforma de monitorización, controlar el árbol de nombres, o incluso operaciones más avanzadas como filtrar los componentes que se monitorizan y los que no se monitorizan.

Una opción muy parecida es la que ofrece Spring JMX o incluso de algún modo lo que ofrece el JDK 6. El problema del segundo es que el soporte no es demasiado avanzado, simplemente es un conjunto de anotaciones para no tener que extender clases o implementar interfaces. El problema del primero es que te obliga a utilizar Spring, algo que quizás no sea posible o simplemente no se quiera añadir por cualquier otra razón.

En los siguientes pasos voy a mostrar una aproximación al uso de anotaciones para este escenario. Lo primero es crear las anotaciones. En este caso serán muy simples a modo de ejemplo.

Anotación para cualquier clase que sea un MBean:
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)
public @interface MBean {
String name();
String root();
} 


Anotación para un atributo instrumentado:

@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.FIELD)
public @interface MBeanAttribute { 
String name();
} 


Anotación para un método instrumentado:
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)
public @interface MBeanMethod { 

}


Una vez definidas las anotaciones ya se podría marcar cualquier clase Java como un MBean. El siguiente código muestra un servicio instrumentado:

@MBean (root="org.test:",name="TestService")
public class TestService {

public static boolean testBoolean = true;
public static String testString = "Test service";

@MBeanAttribute(name="stringAttribute")
public String stringAttribute;
private long lastLong;

public String getStringAttribute() {
return testString;
} 

@MBeanMethod()
public boolean getTestState() {

return testBoolean;
}

@MBeanMethod()
public long customOperation() {

// do some monitoring calcs here
lastLong = Math.round(Math.random()*1000);
return lastLong;
}

public void nonMBeanMethod() {}

public long getLastLong() {
return lastLong;
}
}


Hasta aquí todo bien, pero el que todavía siga leyendo se preguntará... ¿y ahora qué? La idea original era ser capaz de monitorizar nuestro sistema, es decir, cualquier clase Java, sin tener que llenarla de código de monitorización ni obligar a nuestros programadores a utilizar un framework, pero es que hasta este momento el código de monitorización está en nuestro framework imaginario. Así que, ¿cómo se enlaza dicho framework y estas clases Java?

Para hacer eso inyectaremos directamente el código de instrumentación dentro de las clases Java. Esto es lo que normalmente se conoce como weaving, un término relacionado con la programación orientada a aspectos (de hecho todo lo que estoy mostrando aquí se podría realizar también con AOP). Para inyectar el código en este ejemplo utilizaré el framework javassist ya que es bastante descriptivo, pero otros frameworks podrían ser más útiles por ser de bajo nivel como BCEL o ASM. Migrar el ejemplo a estos entornos puede quedar como ejercicio :)

El proceso de weaving de nuestras clases es el más laborioso y la verdad es que resulta difícil poner el código fuente aquí. Voy a escribir a continuación el pseudocódigo del proceso de instrumentación, y podéis leer directamente el código fuente en el enlace de descarga que pongo al final.
  1. Escanear una clase en busca de la anotación @MBean.
  2. En caso de encontrar la anotación, buscar todos los métodos (anotación @MBeanMethod), y los atributos (anotacion @MBeanAttribute) expuestos por la clase que hemos encontrado.
  3. Crear una interfaz utilizando javaassist. Esta interfaz contendrá todos los métodos instrumentados que hemos encontrado, más una serie de métodos get que se generarán para cada uno de los atributos instrumentados (otra opción sería olvidarse de la anotación de atributos y asumir que todo método get que encontremos expondrá un atributo).
  4. A continuación, crear una nueva clase que actuará de proxy e implementará la interfaz que hemos generado en el paso anterior. Esta clase proxy extenderá a StandardMBean, MXBean, o cualquier otra clase que creemos que sea un MBean y que contenga métodos genéricos. Así, en todos los proxies que creemos, tendremos una serie de funcionalidad común.
  5. Crear el constructor del proxy, de forma que se guarde una referencia local al objeto que realmente se quiere monitorizar (esa referencia podría ser una soft o weak reference). El constructor también deberá registrar el managed bean en nuestro framework una vez que ha sido creado.
  6. Implementar todos los métodos de la interfaz en la clase proxy que hemos creado. Estos métodos simplemente delegarán las llamadas sobre el objeto original que se ha recibido en el constructor del proxy.
  7. Por último, modificar el constructor (o los constructores) del objeto original a monitorizar, de forma que cuando se cree una instancia de dicho objeto se cree también un proxy MBean.
El siguiente diagrama muestra como funciona el proceso de weaving. Como lanzar este proceso queda a vuestra elección. Podéis crear un plug-in de maven que simplemente ejecute este algoritmo como parte del proceso de build, o incluso podéis ir más allá y crear un ClassLoader que realize el weaving en tiempo de ejecución cada vez que una clase se carga y registrar ese ClassLoader en vuestro servidor de aplicaciones favorito. La ventaja de este método que he expuesto es que se lo podéis aplicar a cualquier código existente sin tener que añadir ningún tipo de dependencia, tan sólo anotando las clases que ya existen. Pero ojo, también hay alguna desventaja. La principal es que el proceso de weaving modifica las clases originales, con lo que quizás no podréis depurarlas fácilmente. Esto no lo he confirmado aunque probablemente dependa de como funcione javassist. Teóricamente, las modificaciones en el bytecode no tienen porque alterar el proceso de depuración de un programa, aunque añadan nuevo código. Esto es exactamente lo que hacen productos como terracotta. Un JSR específica todo el proceso que garantiza la depuración aunque se modifiquen los bytecodes, pero no recuerdo ahora cual era. Como he comentado anteriormente el código fuente de la parte de weaving es demasiado extenso como para colocarlo aquí. He comprimido todo el código fuente y lo he añadido al final de la entrada. El proyecto ha sido creado con maven 2, así que lo necesitaréis para descargarse las dependencias. Hay un unit test que podéis ejecutar. El test ejecutará todo el weaving para el servicio que he puesto de ejemplo en este artículo y comprobará que los MBeans se registran, que los atributos son accesibles y que se pueden ejecutar los métodos. Podéis ver a continuación varias capturas de jconsole mostrando el servicio anterior.
Si miráis detenidamente la tercera captura de pantalla veréis como el managed bean no es el objeto sino que es el proxy que está delegando todas las operaciones sobre el objeto real. Pues nada, espero que os haya parecido interesante esta idea, sobre anotaciones y weaving de clases. Probablemente el modelo se pueda llevar mucho más allá, como han hecho en terracotta, pero eso ya normalmente dependerá de nuestros recursos y del problema que queramos resolver. Cualquier comentario, ya sabéis. Descárgate el código fuente del ejemplo.

comments

3 Respuestas a "Instrumentando clases Java con anotaciones y class weaving"
Eamonn McManus dijo...
12:52

I covered a similar approach in this blog entry. But using Javassist rather than hand-generated class files seems like a much saner idea. :-)

Éamonn McManus -- JMX Spec Lead --
http://weblogs.java.net/blog/emcmanus


Martín dijo...
13:11

Eamonn, it's the first time I see your entry. Actually... you're absolutely right the approaches are quite similar! I promise I didn't copy you :) So I guess, instrumenting code through annotations it's some kind of natural evolution.

Anyways, if you managed to read the article your Spanish (or your translator) is superb :)


Eamonn McManus dijo...
12:01

I can't claim any credit for superb Spanish -- Google's translator does an excellent job on technical language like this!