Metaprogramación en compile-time con Groovy

En el precedente artículo sobre Metaprogramación en runtime con Groovy explicamos qué es la metaprogramación y vimos las diferentes técnicas que ofrece Groovy de metaprogramación en runtime.

Esta vez lo que aprenderemos son las diferentes posibilidades de metaprogramación en tiempo de colección. Esta clase de técnicas lo que nos van a permitir es intervenir a lo largo de las diferentes fases de colección y, de este modo, producir código en tiempo de colección.

Empezamos con la teoría

El compilador de Groovy tiene 9 fases o bien etapas diferentes que van desde leer los ficheros con el código, parsearlos o bien efectuar las comprobaciones de gramática, hasta la escritura final del bytecode. Para efectuar la colección lo que se crea es un Abstract Syntax Tree (AST) en memoria que no es más que un árbol con nodos y hojas que representa nuestro código. A lo largo de las diferentes etapas de la colección, el compilador de Groovy crea nuevos nodos y hojas, los reordena e inclusive los suprime. Por último el árbol es transformado al bytecode que vamos a poder ejecutar. Estas transformaciones alteran el AST de un programa, es por esta razón que en Groovy se llaman Transformaciones AST. Las transformaciones AST nos dejan intervenir en la generación de este AST para, por último, afectar al bytecode que se produce.

La metaprogramación en compile time deja redactar código que produce bytecode o bien, cuando menos, está implicado a lo largo de la generación del mismo.

Espera, ¿escribiré bytecode?

No, no escribiremos bytecode, vamos a proseguir dejando al compilador que se ocupe de ello. Lo que haremos es alterar el AST para agregar nuevos nodos que representen el código que deseamos incorporar a nuestra clase.

Crear una transformación AST no es una labor fácil, requiere bastantes conocimientos de los internals de Groovy y de de qué forma se representa nuestro código en el AST con lo que vamos a hablar de ConstantExpression, AnnotatedNode, ClassExpression,… y todas y cada una de las clases que usa interiormente el compilador para representar el código que está compilando.
Por poner un ejemplo, el atributo: public static final String VERSION = '2.0' que vamos a ver en un caso se representa como un FieldNode con un ConstantExpression que se pertenece a un ClassNode:

Ventajas de las transformaciones AST

Equiparada con la metaprogramación en runtime la primordial ventaja de esta técnica es que hacemos que nuestros cambios sean perceptibles a nivel de bytecode. Esto implica que:

Si nuestra transformación agrega un procedimiento a la clase, esos cambios van a ser perceptibles si llamamos al procedimiento desde Java o bien otro lenguaje de la JVM. Si empleásemos metaprogramación en runtime los cambios únicamente serían perceptibles desde Groovy.
Al estar la modificación en el bytecode no vamos a tener el overhead de usar Groovy-activo con lo que el desempeño van a ser mejor.
Podemos hacer que la clase implemente una interfaz, extienda de una clase abstracta,… tal y como si verdaderamente escribiéramos ese código.

Tipos de Transformaciones AST

Groovy deja crear 2 géneros de transformaciones AST en función de nuestras necesidades.

Transformaciones Globales: Las aplica el compilador a todo el código conforme lo está compilando sin precisar marcar o bien anotar el código que deseamos afectar por la transformación. Son por lo tanto, trasparentes al desarrollador pues no va a deber alterar su código fuente a fin de que la tranformación sea aplicada.
Es esencial rememorar que este género de tranformaciones se aplican a todo el código con lo que pueden tener gran impacto en el desempeño del compilador.
Transformaciones Locales: Se aplican de forma local anotando el código al que deseamos aplicar la transformación. Seguramente son las más habituales y en nuestro código estamos explicitando nuestras pretensiones por el hecho de incorporar la anotación. Tienen ciertas limitaciones como que no se pueden aplicar en todas y cada una de las fases del compilador, algo que con las globales sí es posible.

Veamos un ejemplo

Tras tanta teoría veremos un tanto de código para procurar comprenderlo mejor. Crearemos una transformación AST Local @Versionar que va a incorporar un atributo estático de tipo String y cuyo valor va a ser el que agreguemos en la anotación.
Voy a enseñar fragmentos de código y los explicaré. Por no hacer los ejemplos larguísimos he quitado todos y cada uno de los imports mas todo el código está libre en Github.

Con esta anotación lo que deseamos hacer es transformar esto:

@demo.Versionar('2.0')
class Foo undefined

en esto (a nivel de bytecode):

class Foo undefined

1. Crear la anotación

package demo

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.TYPE])
@GroovyASTTransformationClass(demo.VersionarASTTransformation)
@interface Versionar undefined

Se trata de una anotación normal de Java, la única diferencia es que con @GroovyASTTransformationClass señalamos la clase en la que vamos a incorporar la transformación AST.

dos. Incorporar la anotación

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
class VersionarASTTransformation extends AbstractASTTransformation undefined

Como veis ocupa apenas treinta líneas de código que vamos a explicar con detalle:

Debemos anotar la implementación con @GroovyASTTransformation señalando exactamente en qué fase del compilador deseamos aplicarla.
Nuestra clase ha de heredar de AbstractASTTransformation y debemos incorporar el procedimiento visit. El compilador se ocupará de ejecutar dicho procedimiento a lo largo de la colección del código.
Primero (punto 1) debemos revisar el género de lo que estamos anotando es adecuado. En nuestro caso estamos equiparando que el nodo anotado (nodes[1]) es una clase por el hecho de que solo deseamos poder utilizar la anotación a nivel de clase y no de procedimiento o bien atributo.
Más tarde (punto dos) es esencial contrastar que el código que deseamos incorporar no existe en la clase. Si no efectuamos esta comprobación y también procuramos incorporar un atributo que ya existe y vamos a tener un fallo de colección.
Por último (punto tres) conseguimos el valor de la anotación y agregamos el campo VERSION que va a ser public, static, final y de tipo String.
Caso de que el valor no sea una incesante vamos a dar un fallo de colección indicándolo.

Para revisar que verdaderamente se ha añadido el campo podemos compendiar la clase con la AST en el classpath y más tarde decompilar el .class. Como podeis ver, nuestro campo public static final String VERSION ha sido añadido a nivel de bytecode.

¿Mas esto tiene utilidad?

Seguramente tras haber visto el ejemplo precedente os estareis preguntando si verdaderamente esto tiene utilidad o bien no, o bien si compensa todo el ahínco. En un caso así se trataba de un caso sencillísimo (¿o bien no tanto?) que sirve de punto de inicio para poder ver el género de inconvenientes que se pueden solucionar con esta técnica.
Con respecto a dónde se utiliza, por poner un ejemplo en Grails o bien en Spock. En el primero para prosperar y incorporar comportamiento a los controllers, servicios,… y en Spock es la base de su potente DSL.

Resumen

Hemos visto una técnica avanzada con la que podemos incorporar soluciones de una forma absolutamente diferente a las más tradicionales. Podemos crear tranformaciones AST que nos dejarán inspeccionar clases para revisar que son thread safety, efectuar comprobaciones ya antes o bien después de ejecutar cierto código o bien aun no ejecutarlo, sin alterarlo. Es verdad que estas técnicas, como las mostradas sobre metaprogramación en tiempo de ejecución son sobre todo utilizadas por frameworks y bibliotecas y no se acostumbran a emplear en el cada día. Todavía siendo eso cierto en mi equipo hemos resuelto inconvenientes en ciertas ocasiones aplicando estas técnicas de una forma muy eficaz, muy elegante y transparente.

Si quereis examinar el código en detalle y revisar de qué forma se puede probar una AST está todo libre en este repo de Github.

Para más información aconsejo echar una ojeada a la documentación oficial sobre transformaciones AST de Groovy.

hljs.initHighlightingOnLoad();

code.hljs undefined
@media only screen and (min-width: 768px) undefined
@media only screen and (min-width: 1024px) undefined

Asimismo te invitamos a

Aportaciones a planes de pensiones, ¿mejor puntuales o bien periódicas?

Kotlin desde la perspectiva de un desarrollador Groovy

Mejora tu código Java utilizando Groovy

– La nueva

Metaprogramación en compile-time con Groovy

fue publicada originalmente en

turincon.net