Introducción a las anotaciones de Java
Qué son las anotaciones de Java, cómo adjuntan metadatos al código y dónde se procesan.
Introducción a las anotaciones de Java
Una anotación es un marcador que adjunta a un fragmento de código fuente Java — una clase, método, campo, parámetro o uso de tipo — y que añade metadatos. La anotación en sí no hace nada. Es una etiqueta que otro fragmento de código (el compilador, un framework, un IDE, una herramienta de compilación) lee más tarde y ante la que reacciona. @Override en un método le dice al compilador: «estoy sobrescribiendo un método de la superclase; protesta si no es así». @Test en un método le dice a JUnit: «esto es una prueba». @Entity en una clase le dice a JPA: «mapea esto a una tabla de base de datos». En todos los casos la anotación solo transporta información; el comportamiento vive en el procesador que la lee.
Ya ha visto anotaciones a lo largo de este libro — @Override en métodos sobrescritos, @FunctionalInterface en interfaces de un solo método, @SuppressWarnings para silenciar al compilador. Esta parte del libro trata del sistema que hay debajo: qué es una anotación, cuándo está disponible (solo en el fuente, en el archivo de clase o en tiempo de ejecución), cómo escribir las suyas y cómo las consumen los procesadores.
La forma de una anotación
Sintácticamente, una anotación es el símbolo @ seguido del nombre de la anotación, aplicado inmediatamente antes de aquello que anota:
@Override
public String toString() { ... }
@Deprecated
public void oldApi() { ... }
@SuppressWarnings("unchecked")
List<String> list = (List<String>) raw;Algunas anotaciones toman elementos (su versión de los campos). Los valores de elemento van entre paréntesis:
@SuppressWarnings("unchecked") // one element, value "unchecked"
@SuppressWarnings({"unchecked", "rawtypes"}) // array of strings
@RequestMapping(path = "/users", method = GET) // two named elementsSi una anotación declara un único elemento llamado value, puede omitir el nombre — @SuppressWarnings("unchecked") es la forma abreviada de @SuppressWarnings(value = "unchecked").
Lo que las anotaciones no son
Tres negaciones que evitan los malentendidos más comunes:
- Las anotaciones no ejecutan código. Escribir
@Cachedjunto a un método no almacena nada en caché. Algo más tiene que buscar@Cachedy añadir el comportamiento de caché. La anotación es una marca, no una función. - Las anotaciones no son comentarios. Los comentarios desaparecen al compilar; las anotaciones son construcciones del lenguaje de pleno derecho. Participan en el sistema de tipos, pueden estar obligadas a vivir en el archivo de clase y pueden leerse en tiempo de ejecución mediante reflexión.
- Las anotaciones no sustituyen al código claro. Una larga pila de anotaciones sobre una clase es densidad de información, no siempre claridad. Los frameworks que se apoyan mucho en las anotaciones (Spring, JPA, JAX-RS) pagan la comodidad con una curva de aprendizaje y un coste en tiempo de ejecución.
Cuándo vive la anotación
Cada anotación tiene una política de retención que decide cuánto sobreviven los metadatos:
SOURCE— el compilador la lee y luego la descarta.@Overridey@SuppressWarningsfuncionan así; el bytecode no conserva ningún rastro de ellas.CLASS— la anotación se escribe en el archivo.classpero la JVM no la carga en tiempo de ejecución. Este es el valor por defecto. Las herramientas que inspeccionan el bytecode (analizadores estáticos, posprocesadores) pueden leerla.RUNTIME— la anotación se conserva hasta el final; la reflexión puede pedirle a cualquier clase, método o campo sus anotaciones en tiempo de ejecución. Frameworks como Spring y Jackson dependen de esto.
Verá la meta-anotación @Retention(...) que fija esta política en el capítulo Meta-anotaciones. En resumen: elija la retención según quién necesita leer la anotación — el compilador, una herramienta en tiempo de compilación o el código en tiempo de ejecución.
Dónde puede ir la anotación
Cada anotación también tiene un destino (target) — el conjunto de elementos de programa que puede anotar legalmente. Los destinos comunes:
TYPE— clases, interfaces, enums.METHOD— métodos.FIELD— campos.PARAMETER— parámetros de método.CONSTRUCTOR— constructores.LOCAL_VARIABLE— declaraciones de variables locales.ANNOTATION_TYPE— otras declaraciones de anotación (meta-anotaciones).PACKAGE— paquetes, víapackage-info.java.TYPE_USE— cualquier uso de un tipo, incluidos los parámetros genéricos y los casts (Java 8+).
Si coloca una anotación donde su @Target no lo permite, el compilador la rechaza. Intentar @Override en una declaración de clase es un error de compilación porque @Override apunta a METHOD.
Quién lee las anotaciones
Tres lugares consumen los datos de anotación:
- El compilador. Las anotaciones integradas como
@Override,@SafeVarargsy@FunctionalInterfacelas comprueba el propiojavac. - Procesadores de anotaciones. Herramientas conectables de tiempo de compilación que se ejecutan durante
javac. Pueden leer las anotaciones de los fuentes que se compilan y generar en respuesta nuevos archivos de código fuente. Lombok, Dagger, el metamodelo estático de Hibernate y el frameworkMicronautfuncionan todos así. - Reflexión en tiempo de ejecución.
Method.getAnnotations(),Class.getAnnotation(...), etc. devuelven las instancias de anotación de cualquier elemento con retenciónRUNTIME. Así decide Spring qué inyectar, así encuentra JUnit sus pruebas y así mapea Jackson el JSON.
Los dos primeros no necesitan más soporte de la máquina virtual que el que ofrece javac. El tercero necesita que la anotación esté escrita en el archivo de clase y cargada por el entorno de ejecución.
Un ejemplo resuelto: inspeccionar las anotaciones de su propia clase
El objetivo de este ejemplo es mostrar que @Override, @Deprecated y @SuppressWarnings lucen idénticas en el fuente pero se comportan de forma distinta una vez compilada la clase. El programa declara una clase con varias anotaciones y luego le pregunta a la reflexión qué puede ver realmente.
Qué llevarse de la ejecución:
- El bucle a nivel de clase vio
@DeprecatedenGreeterpero nada más —@Deprecatedtiene retenciónRUNTIME.@Overridey@SuppressWarningssonSOURCE, así que el compilador las eliminó antes de escribir el archivo de clase y la reflexión no puede recuperarlas. - El bucle por método solo imprimió
@DeprecatedenoldHello. AunquetoStringse declaró@Overrideycastse declaró@SuppressWarnings("unchecked"), ninguna anotación llegó al archivo de clase. La información existió en el momento en que se ejecutójavac— así ocurrió la comprobación de sobrescritura — y luego se descartó. - La comprobación de retención lo dejó claro:
@Overridey@SuppressWarningsllevan@Retention(SOURCE)en su propia declaración, mientras que@Deprecatedlleva@Retention(RUNTIME). La retención es una propiedad del tipo de anotación, no de cómo se usa. - Leer
@DeprecatedenGreetervíacls.getAnnotation(Deprecated.class)devolvió un proxy cuyos métodos de elemento (since(),forRemoval()) devolvieron los valores escritos en el fuente. Esa es la interfaz en tiempo de ejecución hacia los metadatos de anotación: una instancia cuyos elementos son métodos de acceso. - La conclusión para elegir la retención: si el único consumidor es
javac(comprobaciones de sobrescritura, supresión de advertencias), useSOURCE. Si un framework necesita leer la anotación mientras el programa se ejecuta (DI, ORM, enlace JSON), useRUNTIME. El capítulo sobre meta-anotaciones cubre cómo declarar esto para sus propios tipos de anotación.
Lo que viene en esta parte
Los capítulos restantes de esta parte le guían a través de:
- Las anotaciones integradas más comunes en
java.lang—@Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface. - Las meta-anotaciones que configuran las suyas —
@Retention,@Target,@Documented,@Inherited,@Repeatable. - La escritura de tipos de anotación personalizados y su lectura mediante reflexión.
- La API de procesamiento de anotaciones en tiempo de compilación que los frameworks usan para generar código.
El arco va de «qué anotaciones le da el lenguaje» a «qué anotaciones puede construir sobre ellas».
Practice
A teammate writes `@Cached` next to a method and expects the second call to return a cached result. What did they get wrong about annotations?