Las anotaciones de Java y ejemplo de procesador de anotaciones en tiempo de compilación
Escrito por
el .
java
planeta-codigo
Enlace permanente
Comentarios
Las anotaciones añadidas en Java 5 son muy utilizadas por múltiples librerías entre ellas Hibernate, Spring o Immutables. Desde Java 6 se ofrece una API para el procesamiento de las anotaciones en tiempo de compilación que permiten generar archivos de código fuente o emitir mensajes de error. Los procesadores de anotaciones son invocados por el compilador de Java permitiendo extender su comportamiento. En el artículo se muestra una implementación para generar clases que implementan el patrón Builder y otro para realizar comprobaciones.
Las anotaciones fueron añadidas en Java 5 como una forma de enriquecer con información el código. Tienen varios casos de uso, algunas están incorporadas en el propio JDK y las utiliza el compilador como @Override, otras son meramente informativas como @Deprecated, también se utilizan en la generación de documentación Javadoc con taglets, otras se procesan en tiempo de compilación para generar código o archivos dinámicamente al compilar, otras se procesan en tiempo de ejecución. Entre las anotaciones predefinidas incorporadas en el JDK hay algunas más.
En este artículo muestro cómo crear anotaciones para generar errores de compilación propios, como generar código dinámico en tiempo de compilación e integrarlo con un IDE como IntelliJ y con la herramienta de construcción Gradle.
Contenido del artículo
Qué es una anotación en Java
Las anotaciones es una metainformación que se añade en el código fuente. Por sí mismas no hacen nada, es al procesarlas cuando se añade su comportamiento, sirve desde para añadir documentación, realizar comprobaciones de compilación, generar código o programar funcionalidades transversales.
Si se crea directamente una instancia de una clase anotada sin procesar las anotaciones esta no incluye el comportamiento que las anotaciones tienen intención de añadir, es el creador de la instancia el que ha de encargarse de añadirles el comportamiento en el momento de instanciarlas, el procesado de las anotaciones se puede hacer en tiempo de compilación o en tiempo de ejecución.
Este es el código básico de una anotación y su uso en una clase, su definición se realiza con la palabra @interface, se indica a que elementos del código fuente se pueden aplicar y el nivel de retención.
|
|
|
|
|
|
Las anotaciones tienen una sintaxis especial y definen atributos para en el momento de utilización proporcionar valores. Además poseen un nivel de retención según el cual la anotación está disponible:
- Runtime: la información de las anotaciones quedan disponibles hasta en tiempo de ejecución y accesible mediante reflexión con los métodos de la clase Class.
- Class: el compilador emite las anotaciones en tiempo de compilación en los archivos class de bytecode pero no están disponibles en tiempo de ejecución. Puede ser útil para herramientas que procesa los archivos de bytecode.
- Source: las anotaciones son procesadas y descartadas en tiempo de compilación.
Las anotaciones también definen en que elementos del código fuente se pueden indicar:
- ElementType.ANNOTATION_TYPE se puede aplicar en otra anotación.
- ElementType.CONSTRUCTOR se puede aplicar en un constructor.
- ElementType.FIELD se puede aplicar en una propiedad.
- ElementType.LOCAL_VARIABLE se puede aplicar en una variable local.
- ElementType.METHOD se puede aplicar en un método.
- ElementType.PACKAGE se puede aplicar en un paquete.
- ElementType.PARAMETER se puede aplicar en un parámetro.
- ElementType.TYPE se puede aplicar en un tipo.
No es habitual tener que crear un procesador de anotaciones, Spring usa de forma extensiva las anotaciones procesándolas en tiempo de ejecución. Otra posibilidad es usar AspectJ para procesarlas procesarlas después de la compilación a bytecode o ByteBuddy que permite procesarlas en tiempo de compilación o ejecución. Otras librerías que usan anotaciones son las librerías Immutables, Lombok e Hibernate.
- Programación orientada a aspectos con AspectJ, Spring AOP y la clase Proxy
- Generación de código en tiempo de ejecución con Byte Buddy
- Formas de reducir el código de las clases POJO de Java
Procesador de anotaciones
El JDK ofrece una API para el desarrollo de procesadores de anotaciones. Un procesador de anotaciones es una clase que implementa la interfaz Processor, normalmente al crear un procesador de anotaciones se extiende de la clase AbstractProcessor.
Al definir el procesador de anotaciones se indica que anotaciones soporta el procesador y que nivel de código fuente soporta. El compilador de Java al realizar el proceso de compilación invoca a los procesadores de anotaciones proporcionando los elementos de código fuente que los contienen.
El método principal a implementar es el método process, el procesador ha de recopilar la información que necesite a través de los objetos proporcionados en el método y hacer uso de los servicios proporcionados en la clase ProcessingEnvironment. Para generar archivos de código fuente se utiliza el servicio Filer y para emitir mensajes de error el servicio Messager.
Con la infraestructura de servicios de Java se define el procesador de anotaciones creando un archivo de texto en la ubicación META-INF.services/javax.annotation.processing.Processor. El archivo contiene una línea por cada procesador de anotaciones de la librería. Los procesadores de anotaciones también se puede especificar de forma explícita con la opción -processor de javac.
|
|
Generar código fuente
Utilizando el servicio Filer el procesador de anotaciones es capaz de generar nuevos archivos de código fuente. En este ejemplo se muestra como generar una clase que implementa el patrón Builder para la clase Foo anotada con la anotación @Builder. El procesador de anotaciones explora los elementos de la clase y con las propiedades que descubre genera el código fuente de la clase y los métodos adecuados de la clase Builder. El procesador de anotaciones en este caso emite el resultado mediante un PrintStream.
|
|
El resultado del procesador de anotaciones usando Gradle es un archivo de código fuente ubicado en build/generated/sources/annotationProcessor/java/main con el siguiente contenido. Las clases del proyecto pueden hacer referencia a esta clase generada como si existiese en el momento de compilación, los IDE también la detectan como cualquier otra clase.
|
|
El uso de la clase builder es igual que cualquier otra clase del proyecto.
|
|
Realizar comprobaciones de compilación
La anotación @Value es una anotación mediante la cual en tiempo de compilación se comprueba que una clase tiene redefinidos en este caso los métodos equals(), hashCode() y toString(). Es importante implementar correctamente los métodos equals(), hashCode() porque son usados por las colecciones, una implementación de estos que no cumple con los contratos de los métodos da lugar a potenciales errores y comportamientos anómalos. En caso de que la clase anotada no tenga redefinidos estos métodos se emite una advertencia de compilación.
Este procesador de anotaciones hace uso del servicio Messenger que posee métodos para emitir mensajes de error, de advertencia o de información. El procesador busca que métodos tiene la clase anotada y si no cumple la validación emite un error.
|
|
La clase Foo al estar anotada con la anotación Foo pero no redefinir los métodos equals, hashCode y toString heredados de Object el compilador y el procesador de la anotación genera un mensaje de advertencia en la compilación.
|
|
Procesador de anotaciones en Gradle
Para que Gradle utilice los procesadores de anotaciones definidos en una librería hay que declararlo en la sección de dependencias mediante annotationProcessor.
|
|
Esta dependencia se instala en el repositorio de Maven local haciendo uso del plugin maven-publish.
|
|
El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:./gradlew annotationprocessor:publishToMavenLocal && ./gradlew main:run