Javassist: manipulando el bytecode de una aplicación Java.

En este tutorial vamos a ver cómo podemos manipular el bytecode de una aplicación Java con el soporte de la librería javassist, también dentro del ciclo de vida de una aplicación Spring Boot.

0. Índice de contenidos.

1. Introducción.

Javassist es una librería que facilita la modificación del bytecode de una aplicación java. Para ello hace uso de reflexión, permitiendo modificar la implementación de una clase en tiempo de ejecución.

A diferencia de otras librerías del mismo estilo, javassist proporciona dos niveles de api:

  • a nivel de código fuente: que permite editar una clase sin tener conocimientos específicos del bytecode de java, solo necesitas conocimientos del API java, pudiendo insertar bytecode como código fuente normal puesto que javassist lo compila al vuelo,
  • y a nivel de bytecode: que permite editar la clase ya compilada.

Frameworks como Spring e Hibernate han usado librerías como javassist o cglib para generar proxies dinámicos desde sus inicios y desde hace tiempo la propia JDK incluye soporte para la generación de proxies dinámicos. Este tipo de librerías son las que permiten que tanto estos frameworks, como las arquitecturas que se basan en los mismos, puedan hacer su parte de magia, facilitando la vida a los programadores:

  • abriendo una transacción con base de datos cuando un método está anotado con @Transactional o
  • accediendo a base de datos con la simple invocación a un método getUsers de una entidad, si está marcado como lazy.

En este tutorial vamos a ver un ejemplo muy sencillo de su uso que nos va a permitir modificar en tiempo de ejecución el comportamiento de un método de una clase. Después veremos también cómo hacerlo dentro del ciclo de vida de una aplicación Spring Boot.

Estando bajo el paraguas de un framework que soporte Spring o CDI siempre tendremos otras alternativas a nivel de AOP que nos permitirán no tener que llegar a tan bajo nivel. E incluso sin dicho soporte, siempre podríamos hacer uso de AspectJ, modificando el código de nuestra clase para manipularla en tiempo de compilación; pero ¿y si lo que queremos manipular no está bajo nuestro contexto o no lo hemos compilado nosotros?…

2. Entorno.

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Sierra 10.12.5
  • Oracle Java: 1.8.0_25
  • Apache Maven 3.2.3
  • Javassist 3.21.0-GA
  • Spring Boot 1.5.6.RELEASE

3. Manipulando el código de un método en tiempo de carga.

Vamos a ver un ejemplo muy sencillo, un método que recibe una cadena y devuelve un resultado concatenando el parámetro de entrada:

A continuación vamos a hacer un test, también muy sencillo:

En este punto el primer método del test pasa pero el segundo no, el método no devuelve nunca una excepción, siempre saluda del mismo modo al argumento de entrada.

A continuación vamos a manipular el bycode del método para lanzar una excepción concreta en función del parámetro de entrada, en un método anotado con beforeClass

Además de lanzar la excepción hemos lanzado a consola el parámetro de entrada; ahora los dos tests pasan.

Para manipular el bycode debemos recuperar la clase del pool del classloader en una instancia de una clase CtClass (compile-time class), acceder al método en cuestión, manipular el código e invocar al método toClass para convertir la instancia de la clase CtClass a una clase compilada de nuevo.

$1 es un identificador para hacer referencia al primer argumento de un método.

El API es mucho más extesa que este simple ejemplo que hemos hecho, podemos modificar la interfaz de una clase, añadir un constructor, añadir un método, un atributo,…

4. Manipulando el código dentro de una aplicación Spring Boot.

Si en el ejemplo anterior alguien hubiese hecho cualquier tipo de referencia a la clase HelloWorld con anterioridad a los métodos de tests, el compilador de javassist indicaría que la clase ya ha sido cargada y el ciclo de vida de la manipulación del código sería más complejo, puesto que una vez cargada la clase queda marcada como “congelada”.

Si la clase que necesitamos manipular es cargada dentro de ciclo de vida del arranque de una aplicación Spring Boot a continuación mostramos un ejemplo basado en la configuración de un listener de arranque.

La clave aquí está en la recuperación del classloader de spring boot y su asignación al class path de javassist. Sin esa asignación, fuera del contexto de tests, ejecutándose como una aplicación Spring Boot, no encontraría la clase a manipular.

El listener hay que declararlo en un fichero spring.factories dentro de src/main/resources/META-INF/

Y, a continuación, un test de integración para comprobar que todo funciona bajo un contexto Spring Boot:

Et voilà.

5. Referencias.

6. Conclusiones.

Poco uso… y mucho cuidado con el abuso.

Un saludo.

Jose