Modificación de código con Javassist
Recientemente tuve contacto con esta librería, que permite hacer cosas bastante interesantes. El objetivo central de la misma es permitir la manipulación de de clases de Java, directamente sobre los binarios, en tiempo de ejecución.
Dentro de su funcionalidad está la capacidad de leer y modificar las anotaciones que tiene un método o clase, siempre y cuando hayan sido definidas con Retention.CLASS o Retention.RUNTIME, es decir, anotaciones que se quedan en la clase compilada pero son ignoradas por la JVM al momento de utilizar la clase, o bien anotaciones que se quedan en la clase y son visibles en tiempo de ejecución.
Lo interesante es que se pueden ver las anotaciones y otras propiedades de una clase y sus métodos, antes de cargarla a la JVM, esto porque se lee el archivo
directamente y se interpreta, e incluso se puede modificar. Por ejemplo, se pueden agregar anotaciones a una clase que no las tenía.
Incluso parece ser que se pueden modificar métodos, agregando código al principio o al final del mismo; crear clases al vuelo que heredan de clases existentes.
Bueno y de qué nos sirve? Pues por primera vez me encontré con un uso para esto: estoy utilizando Hibernate 3 en un proyecto, y decidí aprovechar las anotaciones de EJB 3 que ahora Hibernate sabe interpretar, y adicionalmente las anotaciones de Hibernate para validar ciertas propiedades de los objetos, como
,
,
,
,
, etc.
En este proyecto también estoy utilizando Tapestry 5, y resulta que hay un componente bastante bueno que facilita la edición completa de beans, por lo que puedo tomar un objeto de los que lei con Hibernate y pasarlo a este componente, para editar sus propiedades. Y dicho componente (llamado BeanEditForm) puede leer una anotación de Tapestry llamada
en la cual se indica si la propiedad es obligatoria o debe tener cierto formato, longitud, etc. Es muy similar a las anotaciones de validación de Hibernate, excepto que sirven para validar en la página, al momento de estar editando un objeto, y muestran al usuario los mensajes de error correspondientes desde el momento en que se deja de editar un campo.
El problema es que no quiero tener que poner dos validaciones en cada propiedad de mis objetos, primero porque aparte de ser dependientes de Hibernate, los voy a hacer dependientes de Tapestry, con lo cual estoy rompiendo con la buena práctica de bajo acoplamiento (o sea que los distintos módulos de un sistema dependan lo menos posible unos de otros), y segundo porque apenas estoy empezando con el proyecto y si despues hay que agregar más entidades o más propiedades a entidades existentes, hay más probabilidades de cometer errores y olvidar ya sea la validación de Tapestry o la de Hibernate.
Y realmente esto suena como algo que se puede automatizar. Hay varias opciones: modificar el BeanEditForm de Tapestry para que no solamente lea los @Validate de Tapestry sino que también considere las anotaciones de Hibernate, pero entonces ese componente va a depender de Hibernate y no todo mundo lo usa.
Por lo tanto me puse a experimentar con Javassist, haciendo un ServletContextListener para que al levantar mi aplicación, este listener encuentre en las librerías de la aplicación las clases que representan entidades y cuando encuentre cada una, le busque los métodos que tengan anotaciones de validaciones de Hibernate, y les agregue a esos métodos la anotación correspondiente de Hibernate, de modo que si encuentra un método que traiga la
de Hibernate, le agregue una
. Hay algunos métodos que traen más de una anotación, por ejemplo
y también
o
. En esos casos hay que poner una sola anotación de Tapestry que contenga todas las restricciones, por ejemplo
.
La verdad es que sin Javassist no hubiera podido lograr esto. Ahora las clases que son entidades de Hibernate y traen validaciones de Hibernate, al correr la aplicación tienen las mismas validaciones pero para Tapestry, lo cual permite que el BeanEditForm muestre al usuario errores de validación sin tener siquiera que hacer un submit. Y no tenemos que ponerle dos anotaciones de validación distintas a cada propiedad, sino solamente la de Hibernate.
Lo más complicado fue hacer que se carguen las clases modificadas, ya que hay que fijarse en cargar primero las que no tengan dependencias con otras entidades y después ya cargar las que tienen dependencias entre ellas (sobre todo con las que ya fueron cargadas). De otra forma puede haber problemas porque de alguna forma se carga la clase sin modificar y entonces ya no se puede cargar posteriormente la clase ya modificada. Aunque vi que hay una manera de hacer "hot swap" y sobreescribir la clase original con la modificada incluso despues de haber sido cargadas, prefiero que se carguen una sola vez (todo esto para mi está un pasito atrás de ser magia negra).
Voy a evaluar la posibilidad de abrir este código y contribuirlo a alguna librería de Tapestry, o de hacer una clase similar con la misma funcionalidad en mi tiempo libre para poder compartir esto a otros proyectos, porque estoy seguro que no soy el primero que se topa con este problema.
Página oficial del proyecto:
UPDATE: Ya me comuniqué con Howard Lewis Ship, el autor de Tapestry, y me dice que para la versión 5.1 de Tapestry tienen planeado incluir algo que mapee las validaciones de Hibernate con las de Tapestry, por lo que van a hacerlo de manera distinta la solución que hice.
- ezamudio's blog
- Inicie sesión o regístrese para enviar comentarios
Otras librerías
Hola ezamudio, sería de gran utilidad si publicaras esa porción de código para verla .... pienso que encajaría perfecto en tapestry-hibernate
integration.
Como bien comentas, sería de gran utilidad para no tener que estar repitiendo tantas anotaciones y reutilizar las de hibernate, ya que podríamos llegar al punto donde en lugar de tener XML-itis podríamos llegar a tener @-tis ... :)
De acuerdo a tu comentario sobre javaassist, la librería me suena a algo para implementar la AOP ya que está basada en el concepto de interceptar/modificar el bytecode
en tiempo de ejecución.
Otras librerías similares e interesantes a javaassist son:
- asm la cual tiene un soporte a bajo nivel del bytecode con la desventaja que no es muy fácil de utilizar.
- Cglib la cual es el cascarón de asm (Que porcierto tiene una clase llamada FastClass la cual permite hacer reflexión de una forma más óptima)
- Proxys nativos de la plataforma java, aunque tienen la desventaja que tus clases deben implementar cierta interfaz para poder utilizar su funcionalidad
- De cierto modo y de forma muy simple la clase DynaBean/DynaClass de la librería BeansUtils, la cual utilizo para inyectar propiedades dinámicas a clases en tiempo de ejecución (Simulado).
Spring utiliza todos esos esquemas para implementar la AOP internamente y me parece que se puede cambiar de implementación a implementación con solo con modificar una de sus configuraciones.
Javaassist puede utilizarse como implementación AOP en spring ? o simplemente son cosas distintas ?
JavaRanch big moose saloon member
Javassist y Spring...
Spring no tiene ninguna dependencia con Javassist. Spring para AOP usa AspectJ, los proxies dinámicos que mencionas de J2SE, o CGLIB.
Si bien es cierto que Javassist se puede usar como base para hacer algo de AOP, no es tal cual una libreria de AOP, sino una librería de manipulación de bytecode en tiempo de ejecución. Pero bien se podría armar un framework de AOP que utilice Javassist para la modificación de las clases.
Tapestry usa Javassist para modificar las clases que corresponden a tus páginas y componentes, porque es como procesa todas las anotaciones de @Inject, @Service, @InjectPage, etc y además tu POJO lo mete dentro de otra clase o algo raro hace para que funcione la página pero todo es interno, no tienes que preocuparte por nada de eso.
Voy a revisar eso de tapestry-hibernate-integration a ver qué se puede hacer.
Given the choice of dancing pigs and security, users will choose dancing pigs, every single time. - Steve Riley