Meta-anotaciones en Java
Anotaciones que anotan otras anotaciones en Java — @Retention, @Target, @Documented, @Inherited, @Repeatable.
Una meta-anotación es una anotación que se coloca sobre la declaración de otra anotación. Configuran cómo se comporta esa anotación: cuánto tiempo sobrevive, dónde puede aparecer, si las subclases la heredan y si se puede aplicar más de una vez. Existen cinco en java.lang.annotation, y todo tipo de anotación que escribas utilizará al menos las dos primeras.
Las cinco:
@Retention— controla si la anotación se conserva en los archivos.classy en tiempo de ejecución.@Target— restringe los tipos de elementos del programa que la anotación puede decorar.@Documented— hace que la anotación aparezca en el Javadoc generado.@Inherited— hace que las subclases hereden las anotaciones a nivel de clase de su superclase.@Repeatable— permite aplicar la misma anotación más de una vez al mismo elemento.
@Retention
@Retention elige uno de tres valores de RetentionPolicy:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.SOURCE) // stripped by javac; never in bytecode
@interface SuppressEvenMoreWarnings { }
@Retention(RetentionPolicy.CLASS) // in bytecode; not loaded by the VM (the default)
@interface BytecodeOnly { }
@Retention(RetentionPolicy.RUNTIME) // in bytecode and accessible via reflection
@interface MyRuntimeMarker { }La elección correcta depende del consumidor:
- El compilador es el consumidor (comprobaciones de anulación, supresión de advertencias, lint) →
SOURCE. - Una herramienta de procesamiento de bytecode, un optimizador JIT o un analizador posterior a la compilación es el consumidor →
CLASS. - Un framework lee la anotación en tiempo de ejecución mediante reflexión (DI, ORM, enlace JSON) →
RUNTIME.
RUNTIME es la más permisiva — también es la más costosa, ya que cada anotación que sobrevive añade bytes al archivo de clase y un pequeño coste de reflexión en el momento de la búsqueda.
@Target
@Target restringe dónde puede colocarse la anotación. Su valor es un array de constantes de ElementType:
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@interface Audited { } // only on methods or constructors
@Target(ElementType.TYPE)
@interface Entity { } // only on classes/interfaces/enums
@Target({})
@interface CannotBeApplied { } // exists only as a type — can't be used to annotate anythingLos valores de ElementType que encontrarás:
TYPE— clase, interfaz, enum, anotación.METHOD,CONSTRUCTOR,FIELD,PARAMETER,LOCAL_VARIABLE.ANNOTATION_TYPE— para meta-anotaciones como las de este capítulo.PACKAGE— enpackage-info.java.TYPE_USE(Java 8+) — cualquier uso de un tipo, incluidos los casts ((@NonNull String) o), cláusulasextendsy argumentos genéricos. Utilizado por verificadores de nulabilidad como Checker Framework.TYPE_PARAMETER(Java 8+) — solo en declaraciones<T extends ...>.MODULE(Java 9+) — enmodule-info.java.RECORD_COMPONENT(Java 16+) — en los parámetros de la cabecera de un record.
Si omites @Target por completo, la anotación puede ir casi en cualquier lugar — útil para marcadores muy generales, pero restrictivo para todo lo demás. Establece siempre un @Target explícito.
@Documented
Por defecto, las anotaciones no se muestran en el Javadoc de los elementos que decoran. @Documented hace que una anotación se incluya:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ApiNote {
String value();
}Si un método lleva @ApiNote("rate-limited to 5 rps"), Javadoc mostrará esa anotación en la documentación generada. Sin @Documented, la anotación existe en tiempo de ejecución pero es invisible en la documentación generada. Añade @Documented a todo lo que esperes que los usuarios de tu biblioteca vean.
@Inherited
@Inherited se aplica únicamente a anotaciones que apunten a TYPE (clases). Significa que: si una clase está anotada, sus subclases se tratan como si también estuvieran anotadas. El método getAnnotation(...) de reflexión recorrerá la cadena de superclases hasta encontrarla.
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Auditable { }
@Auditable
class Account { }
class SavingsAccount extends Account { } // also "auditable" via inheritanceAdvertencias:
- Solo recorre la cadena de superclases — las interfaces no propagan anotaciones aunque usen
@Inherited.class Foo implements Auditable { }no hace queFoolleve un@Auditableproveniente de la interfaz. - Solo afecta a cómo la reflexión informa sobre anotaciones en clases. Los métodos, campos y parámetros nunca heredan anotaciones de los miembros anulados.
La mayoría de los frameworks prefieren ahora anotaciones explícitas y repetidas en lugar de la herencia, porque las reglas son más simples. Usa @Inherited solo cuando "cualquier cosa que extienda una clase marcadora también esté marcada" sea realmente lo que necesitas.
@Repeatable
Antes de Java 8, no se podía aplicar la misma anotación dos veces al mismo elemento. @Repeatable levanta esa restricción, pero la mecánica requiere cuidado. Se declara una anotación contenedora que contiene un array de los valores repetidos, y se apunta @Repeatable al contenedor:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules { Schedule[] value(); } // the container
@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule { String cron(); } // the repeated annotation
class Reports {
@Schedule(cron = "0 9 * * MON")
@Schedule(cron = "0 9 * * FRI")
public void weekly() { /* ... */ }
}Reglas:
- El elemento
valuedel contenedor debe ser un array del tipo de anotación repetida. - El contenedor y la anotación repetida deben tener la misma retención y al menos los mismos targets.
- Si la anotación repetida es
@Documented, el contenedor debe ser también@Documented— e igualmente para@Inherited. El compilador rechaza una discrepancia concontaining annotation interface ... is not @Documented. Mantén sus meta-anotaciones sincronizadas. - En tiempo de ejecución, el compilador agrupa los usos repetidos en una única anotación contenedora. La reflexión ofrece tanto
getAnnotation(Schedule.class)(devuelve el único elemento del contenedor cuando hay dos) comogetAnnotationsByType(Schedule.class)(devuelve el array directamente). Usa el segundo para anotaciones@Repeatable.
Un ejemplo completo: las cinco meta-anotaciones en una anotación real
El ejemplo construye un pequeño sistema de anotaciones de principio a fin: un @Schedule que es RUNTIME, solo para métodos, documentado y repetible; un marcador @Module que las subclases heredan. El método main las lee mediante reflexión.
Lo que se puede aprender de la ejecución:
@Modulefue declarado en la superclaseReportingBase, pero la reflexión lo encontró enWeeklyReportsporque la anotación lleva@Inherited. La búsqueda recorre la jerarquía de clases hasta encontrar la anotación o agotar las superclases.- El caso de la interfaz mostró el límite de la herencia:
WithInterfaceimplementaAnnotatedInterface, que tiene@Module, perogetAnnotation(Module.class)devolviónull.@Inheritedsolo recorreextends, nuncaimplements. Esto confunde a muchos principiantes; si necesitas visibilidad de anotaciones entre interfaces, debes recorrer el árbol de tipos tú mismo. runWeeklyllevaba dos anotaciones@Schedule.getAnnotationsByType(Schedule.class)devolvió un array de longitud 2 — la forma correcta de leer anotaciones repetidas. El contenedor@Scheduleses invisible para el código de usuario si te ciñes agetAnnotationsByType.- El caso de
@Scheduleúnico enrunDailyfue simétrico:getAnnotation(Schedule.class)funcionó porque no había contenedor, ygetAnnotationsByTypedevolvió un array de longitud 1. Cualquier forma es válida cuando conoces la multiplicidad. - Las líneas del "caso repetido mediante
getAnnotation" expusieron la trampa. EnrunWeekly,getAnnotation(Schedule.class)devolviónull— la anotación real en el archivo de clase es un contenedor@Schedulessintetizado, no unSchedule. Acceder al contenedor mediantegetAnnotation(Schedules.class)sí funciona. La regla: para cualquier anotación@Repeatable, usa siempregetAnnotationsByTypepara que los dos casos (una ocurrencia vs. varias) se vean idénticos.
Elegir tu conjunto
Cuando escribes una nueva anotación, decide las cinco a la vez:
- ¿Quién necesita leerla? →
@Retention. - ¿Dónde puede aparecer? →
@Target. - ¿Deben verla los usuarios en el Javadoc? →
@Documentedo no. - ¿Deben heredarla las subclases? →
@Inheritedpara marcadores a nivel de clase como@Auditable. Omítelo para anotaciones a nivel de método. - ¿Debe aplicarse más de una vez? →
@Repeatablesi y solo si realmente necesitas multiplicidad.
El esqueleto predeterminado para una anotación de método visible en tiempo de ejecución:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@interface MyAnnotation { /* elements */ }El siguiente capítulo — Anotaciones personalizadas — utiliza exactamente este patrón para construir una desde cero y consumirla con reflexión. Para las anotaciones que incluye el JDK, consulta Anotaciones integradas; para la API de reflexión utilizada anteriormente, consulta Lectura de anotaciones con reflexión.