Anotaciones, levantando el velo

[English version]Este enlace se abrirá en una ventana nueva

De forma recurrente cuando hago entrevistas, he percibido que quizá sean las anotaciones una de las características más usadas pero menos conocidas del lenguaje Java.

Por una parte, creo que, más o menos, todo el mundo que trabaje con Java hoy en día esta acostumbrado a usarlas en librerías o frameworks como Spring o Hibernate. Pero, esa utilización no se ve acompañada por la capacidad de explicar lo que son, si tiene sentido crear anotaciones personalizadas y que hay detrás de Hibernate o Spring.

annotationsannotations

Las anotaciones se relacionan con un concepto que es la meta-programación. De forma muy poco ortodoxa, podríamos definir a una anotación como algo que permite etiquetar declaraciones (campos, métodos, parámetros y clases) con cierta información. Esta información será utilizada por un procesador externo que leerá esos datos y decidirá que hacer con ellos.

Siendo sincero, no es una definición muy brillante, y seguro que os preguntáis de que tipo de control os estoy hablando. Creo que no hay nada mejor que una serie de ejemplos, así que aquí van:

  • Nos permiten extraer información de la clase, para aplicar conceptos como serializar (transformar el objeto a una representación plana). Un ejemplo sería GsonEste enlace se abrirá en una ventana nueva y sus anotaciones pesonalizadas.
  • Nos posibilitan facilitar la validación de un parámetro (verificar si es nulo, si está vacío, o si cumple un determinado patrón o expresión compleja) e incluso la modificación de su valor antes de invocar el método. Ejemplos básicos serian las anotaciones @NotNull o @NotEmpty.
  • Son útiles para crear un motor plug and play. Este motor podría utilizar anotaciones para detectar (de forma dinámica o estática) las clases que deben ser conectadas. Cuando utilizas endpoints en SpringEste enlace se abrirá en una ventana nueva, estáis de hecho "conectando" clases al motor de controladores de Spring.
  • Se pueden emplear para modificar la lógica de la clase. Esta modificación puede hacerse mediante un ProxyEste enlace se abrirá en una ventana nueva o modificando el bytecode con la ayuda de librerías como ASMEste enlace se abrirá en una ventana nueva.

De todas estas la más compleja es la última, no os recomiendo hacerlo salvo que no os quede más remedio que manipular el bytecode – como fue en nuestro caso –, es una técnica no especialmente sencilla y sujeta a multitud de puntos de fallo. De todas formas los demás tipos de anotaciones (incluso las basadas en Proxy) son bastante sencillas, proporcionan valor, y además os permiten adquirir un mayor conocimiento del lenguaje Java (y otros relacionados).

A continuación os muestro un pequeño ejemplo del uso de anotaciones y porque nos permiten resolver problemas.

El problema

Imaginaos que queremos rellenar una Base de datos partiendo de una serie de ficheros. Indudablemente tendremos que resolver muchas cuestiones, aunque quizá destacaría dos:

  • ¿Manejamos distintos tipos de información?
  • ¿El proceso de relleno de datos implica la posibilidad de modificar registros?

Vamos a considerar que la respuesta es afirmativa a ambas cuestiones. Tendríamos que dar respuesta a problemas relacionados con como almacenar objetos de tipos distintos y como detectar si un registro se ha modificado. El primero de los problemas pasaría por usar, por ejemplo, un almacén basado en BLOBS o una Base de datos NoSQL.

El otro problema es distinto, detectar si un objeto ha mutado puede resolverse de varias formas, para mi una de las más sencillas es generar un HASH (MD5 o SHA1) del objeto y comparar el hash del objeto origen con el del objeto destino.

Como sabéis la generación del hash no es complicada, pero debéis tener en cuenta que un objeto no es sólo propiedades sino que se almacena otra información como el estado o campos calculados. Lógicamente, el estado y quizá incluso los campos calculados, no forman parte del hash, pero quizá si queramos almacenarlos en la Base de datos.

El prototipo que usaré en todas las soluciones que os voy a contar es en todos los casos el mismo, un interfaz que será responsable de implementar el hash.

Imagen 1Imagen 1

Solución sin anotaciones personalizadas

En este caso utilizaremos Gson y su soporte de anotaciones como @Expose. Esta aproximación es simple y funciona francamente bien:

Imagen 2Imagen 2

Imagen 3Imagen 3

Sin embargo, adolece de dos problemas fundamentals:

  • Por una parte, tendremos que etiquetar todos los campos que queramos serializar, podríamos olvidarnos de algo importante.
  • Por otra parte hay un problema de semántica. La anotación @Expose al ser general no nos da información de la razón de su uso dentro del ámbito del presente problema. Si bien podríamos incluir una documentación muy detallada, ni siquiera con esto tendríamos garantías de que el que lo lea o modifique sea consciente de porque se ha hecho así.

Solución sencilla con anotaciones personalizadas

En este caso utilizaremos una anotación personalizada @HashHidden que incluiremos dentro de Gson. Esta aproximación es casi tan simple como la anterior, y además nos permite resolver los problemas que antes habíamos presentado.

Imagen 4Imagen 4

Imagen 5Imagen 5


Como veis, no solo se introduce una nueva keyword, @interface, sino que también tenemos que utilizar dos anotaciones estándar de Java que nos definen como nuestro @HashHidden va a funcionar: @Retention y @Target. Os lo cuento en más detalle.

  • La anotación @Retention nos indica desde dónde la información de nuestro @HashHidden es accesible, en concreto:
    • RUNTIME: Para mi la más común, se almacena junto con el código compilado y puede accederse a ella desde nuestro código en tiempo de ejecución, utilizando reflexión e introspección.
    • CLASS: La información se almacena con la clase pero no es accessible vía reflexión. Diría que es un caso de uso complejo, sólo las he utilizado cuando he tenido que manipular el bytecode con ASM.
    • SOURCE: La información solo esta disponible en el momento de compilación. No os voy a engañar, nunca he usado este tipo de anotación.
  • Por otra parte, la anotación @Target nos permite controlar en que definiciones podemos utilizar nuestro @HashHidden, pudiendo ser a nivel de clase, método, campo o parámetro.

Solución más compleja y viendo las tripas

Si lo que os interesa es ver las tripas para ver como la magia de las anotaciones funciona, creo que esta solución es para vosotros. Si, por otra parte, vuestro problema es distinto al que os he contado antes, que seguro que si, tened en cuenta que las soluciones basadas en generar un JSON no os van a valer y necesitareis saber un poco más.

En este caso partimos de nuestro estupendo @HashHidden y utilizaremos reflexiónEste enlace se abrirá en una ventana nueva para recuperar los campos, sus valores y sus anotaciones.

Imagen 6Imagen 6

Como podéis ver se trata de un ejemplo muy simplificado, en el cual los valores se transforman a cadenas. Pero creo que os puede dar una idea de cómo pueden utilizarse las anotaciones de una forma algo más compleja.

Algunas ideas más

Una anotación no es solo un cascarón vacío, sino que se pueden definir campos que tienen propiedades tales como:

  • Pueden ser de tipo primitivo, enumeraciones o incluso anotaciones anidadas
  • Pueden ser arrays
  • Hay un tipo de campo denominado "value". Nos permite omitir el nombre del campo cuando se anota una declaración poniendo sólo el valor.
  • Los valores permiten almacenar lo que quieras, por ejemplo una expresión en un lenguaje como MVEL.

De alguna forma podéis imaginar el uso de las anotaciones como si crearais un JSON en un lenguaje como JavaScript

Como las usamos

En Divisa iT hemos utilizado anotaciones con profusión dentro de nuestro framework, como por ejemplo:

  • Notificaciones PUSH: usamos anotaciones para serializar el objeto.
  • Cache: las anotaciones nos permiten detectar dónde hay caché activa, modificando el bytecode inyectando lógica de negocio.
  • Selección de código en función de la BBDD: Usamos un patrón repositorio con soporte multi base de datos. Las anotaciones nos permiten seleccionar el código a ejecutar en función de la base de datos destino.
  • Excepciones: Traduciéndolas a un formato centrado en el usuario.
  • Servicios plug and play: endpoints, servicios de lógica de negocio, lógica de replication y clustering.

Créditos

Los iconos utilizados son cortesía de Flaticon