style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Transformación de clases al vuelo con Javassist

En esta ocasión quiero describir un proceso un tanto complicado, que puede servirle a alguien tal vez, si se encuentran en la necesidad de hacer algo locochón como lo que tuve que hacer yo.

En términos generales, me encontré en la necesidad de agregar anotaciones a clases, en tiempo de ejecución. Es decir, una clase que no tiene ciertas anotaciones, porque no fue compilada así, necesita que se las agreguemos a la hora de correr una aplicación. Esto fue posible gracias a Javassist, una biblioteca de software libre que sirve precisamente para transformar clases en tiempo de ejecución, pero aún así el código y la manera de hacerlo es algo complejo.

Primero que nada, necesitamos el JAR donde se encuentra la clase que queremos modificar. Dependiendo del tipo de aplicación, la manera de obtener el JAR va a variar, pero lo importante aquí es que tengamos al final un InputStream del cual vamos a leer la clase. Una vez que tenemos el InputStream, debemos ir leyendo del JAR hasta obtener el archivo que queremos (un .class).

Una vez que encontremos la clase que nos interesa, necesitamos cargarla pero no con un ClassLoader normal, sino con ayuda de Javassist.

JarInputStream jarin = new JarInputStream(inputStream);
//Este lo vamos a necesitar en varios metodos
ClassPool cpool = new ClassPool();

do {
  entry = jarin.getNextJarEntry();
  if (entry != null && !entry.isDirectory()) {
    String nombre = entry.getName();
    if (nombre.endsWith(".class") && esLaClaseQueBuscamos) {
      //Leer la clase, sin cargarla
      CtClass clase = cpool.makeClass(jarin);
      //Procesar esta clase porque es candidata
      if (procesaClase(clase)) {
        mods.put(clase.getName(), clase);
      }
    }
  }
} while (entry != null);

Este fragmento de código lee el contenido del Jar, clase por clase, y al encontrar la clase que nos interesa (puede ser una, o varias), la carga al ClassPool de Javassist y la procesa. En qué consiste el procesamiento? Como mencioné al principio, vamos a agregarle unas anotaciones. Para el ejemplo, supongamos que nos interesan clases que tengan la anotación @Entity solamente, y le vamos a poner una anotación adicional a uno de los métodos de la clase. Para esto nos vamos a apoyar en más clases de Javassist:

private boolean procesaClase(CtClass clase) {
  boolean cargar = false;
  ClassFile cfile = clase.getClassFile();
  AnnotationsAttribute anns = (AnnotationsAttribute)cfile.getAttribute(AnnotationsAttribute.visibleTag);
  if (anns != null) {
    Annotation entidad = anns.getAnnotation("javax.persistence.Entity");
    if (entidad != null) {
      @SuppressWarnings("unchecked") //esto para que no la haga de tos el compilador con la asignación
      List<MethodInfo> metodos = cfile.getMethods();
      for (MethodInfo method: metodos) {
        anns = (AnnotationsAttribute)method.getAttribute(AnnotationsAttribute.visibleTag);
        if (elMetodoCumpleAlgunCriterio) {
          Annotation nueva = new Annotation("com.ejemplo.MiAnotacion", anns.getConstPool());
          anns.addAnnotation(nueva);
          cargar = true;
        }
      }
    }
  }
  return cargar;
}

Este método obtiene las anotaciones de la clase y busca si tiene la anotación @Entity, en cuyo caso recorre los métodos de la clase y con los que cumplan algún criterio, va a ponerles una anotación adicional @MiAnotacion.

Solamente nos falta ver cómo cargar esta clase que hemos modificado al vuelo, con el método cargaClase:

private void cargaClase(CtClass clase) {
  try {
    //Esto es lo unico que necesitamos hacer
    Class mod = cpool.toClass(clase);
    //Esta llamada es para activar el JIT
    String real = mod.newInstance().getClass().getName();
  } catch (InstantiationException ex) {
    log.error(String.format("Instanciando clase ya modificada %s", clase.getName()), ex);
  } catch (IllegalAccessException ex) {
    log.error(String.format("Instanciando clase ya modificada %s", clase.getName()), ex);
  } catch (CannotCompileException ex) {
    log.error(String.format("No pude modificar clase %s", clase.getName()), ex);
  }
}

Y bien, ese es un ejemplo (algo simple) de cómo modificar clases al vuelo. Obviamente la clase no se modifica en el JAR, solamente fue modificada en memoria. Este tipo de transformaciones no es común hacerlo en aplicaciones, es más común que se realice en frameworks, pero esto puede servir en algunos casos en que no se tiene el código fuente a cierta clase.

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 gabo

Utíl para??

Orales, no pensé que se pudiera manipular a nivel del ByteCode.

Entonces esto es útil cuando por ejemplo no tengas el código fuente de un  .class lo cual no debería suceder??? o la necesidad de hacerlo se da también en otro contexto?

Imagen de luxspes

Javassist: Util para cambios dinamicos

Bueno, si no tienes un .class, y necesitas corregir algo del código, mas bien te conviene un descompilador . Javassist es mas bien para cuando quieres modificar dinamicamente una clase (por ejemplo, para darle capacidades de persistencia sin tener que ponerle código directamente o obtener meta-informacion sobre ella sin tener que usar reflection... de hecho creo que Hibernate lo usa para eso...) (por otro lado supongo que podrías construir un decompilador usando Javassist... )

Imagen de ezamudio

Ejemplo: Tapestry y Hibernate

Esto lo use yo porque en mis clases de entidades les puse anotaciones de Hibernate Validation. Tapestry tiene sus propias anotaciones para validaciones, que tienen la misma finalidad, sin embargo por una parte no quiero estar poniendo anotaciones dobles en mis clases y por otra parte no quiero que tengan una dependencia con Tapestry (porque asi luego podemos cambiar de framework para GUI si es necesario).

De modo que las clases tienen solamente las anotaciones de Hibernate Validation y en tiempo de ejecucion les agrego la anotacion para validacion de Tapestry, equivalente a la que traigan de Hibernate. Esto ya no sera necesario cuando tanto Tapestry como Hibernate usen las nuevas anotaciones estandar para validacion, pero sera hasta Tapestry 5.2 y quien sabe cual version de Hibernate.

Imagen de rodrigo salado anaya

que cosa!!!!

Esto es muy interesante. Este tipo de publicaciones son las que más me gustan, me tendré que leer todo el tutorial y aprender mas, que falta me hace..... muy bueno e interesante.

Imagen de iberck

Desgraciado Javassist

Muy útil, pero desgraciadamente Javaassist es algo que dá muchos dolores de cabeza en Tapestry... existen incluso "prácticas" para evitar las latosas TransformationException
buaaaaaa
iberck

Imagen de ezamudio

Como qué?

Tal vez es porque no he trabajado tanto en Tapestry, pero no he tenido problemas de TransformationException... aunque he sido muy obediente en cuanto a la manera de hacer mis componentes:

  • Clases públicas normalitas, nada de final ni nada
  • propiedades todas private y anotadas con @Property
  • Evitar constructores con argumentos (incluso sin argumentos)
  • No inicializar las variables de instancia en donde se declaran

Claro que los dos últimos puntos resultan algo latosos, pero esa es otra historia.

Imagen de iberck

NO - "reglas clásicas" vs "reglas castrosas"

No se trata de ser obediente con las reglas "clásicas" de tapestry, me refiero a prácticas para que javassist no se vuelva loco:

# Utilizar getter/setter en lugar de la anotación @Property,
# No poner mucho código en el método onActivate() , en lugar
de eso crar un método y llamarlo desde onActivate()
# No poner mucho código en el método setupRender()

A esos límites llega y creeme que es un verdadero infierno encontrar los errores y corregirlos, esto sucede a menudo a la hora de montar el sistema en producción por ejemplo con Tomcat o Glassfish ...., con jetty es muy dificil que aprezcan este tipo de problemas

iberck

Imagen de ezamudio

Versión Javassist

iberck, suena como que el problema es directamente con javassist, estás usando la versión más reciente? ya les has reportado estos problemas a ellos (y a Tapestry)?

Imagen de iberck

Problema conocido - Tapestry

Si, este es un problema ajeno a Tapestry, es bronca de Javasisst (Otra mala decisión aparte de prototype) ...
La versión que utilizo es de la que depende Tap 5.1.0.5, no se si es la más reciente

Y sí, este es un problema conocido en Tapestry (desconozco si también en todos los frameworks que utilizan la librería),
La buena noticia es que en estos momentos Howard está haciendo hasta lo imposible por sacar de una patada a Javasisst de su web framework

iberck

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">