Java Reflection: Lectura de Anotaciones
Lee metadatos de anotaciones en tiempo de ejecución con reflection en Java: getAnnotation, getAnnotations y más.
Los capítulos anteriores cubrieron la declaración de anotaciones (ver Anotaciones Personalizadas y Meta-Anotaciones); este capítulo trata sobre leerlas en tiempo de ejecución mediante reflection. Una anotación con @Retention(RUNTIME) se vuelve consultable en la Class, Method, Field, Constructor y Parameter a los que está adjunta. Leer anotaciones es cómo JUnit encuentra @Test, cómo Spring encuentra @Autowired y cómo los frameworks de validación encuentran @NotNull. Este capítulo reúne toda la API de lectura en un solo lugar, incluyendo los matices de @Inherited y @Repeatable.
Esta página cubre los cuatro métodos de lectura de AnnotatedElement, por qué la retención es un prerequisito obligatorio, cómo @Inherited y @Repeatable cambian lo que se obtiene, cómo leer anotaciones de parámetros y un ejemplo de escáner de validación que puedes ejecutar.
Los cuatro métodos de lectura
Cada elemento anotable (Class, Method, Field, Constructor, Parameter) implementa AnnotatedElement, que define los mismos cuatro métodos en todas partes:
AnnotatedElement el = SomeClass.class; // or a Method, Field, etc.
el.isAnnotationPresent(Audited.class); // boolean — quick check
el.getAnnotation(Audited.class); // the annotation instance, or null
el.getAnnotations(); // ALL annotations (declared + inherited)
el.getDeclaredAnnotations(); // only those declared directly hereComo la API es uniforme, el código para leer una anotación de un método es idéntico a leerla de una clase — simplemente se usa un AnnotatedElement diferente. Los valores de anotación recuperados son proxies generados por la JVM; llamar a a.value() es una llamada real al método que devuelve el valor del elemento compilado en tiempo de compilación.
La retención es un prerequisito
Esto merece repetirse porque es el error número uno: solo las anotaciones retenidas con RUNTIME son visibles para reflection.
@Retention(RetentionPolicy.RUNTIME) // <-- required for reflection
@interface Audited { String value(); }La retención predeterminada es CLASS, que mantiene la anotación en el archivo .class pero la descarta antes del tiempo de ejecución. La retención SOURCE la elimina incluso antes. Si getAnnotation devuelve null sobre una anotación que claramente se ve en el código fuente, la falta de @Retention(RUNTIME) es casi siempre la causa.
getAnnotations vs getDeclaredAnnotations y @Inherited
La diferencia entre estos dos es @Inherited. Por defecto, las anotaciones no son heredadas por las subclases. Pero si un tipo de anotación está meta-anotado con @Inherited, entonces una subclase hereda una anotación de nivel de clase de su superclase:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Component { }
@Component class Base { }
class Derived extends Base { } // Derived has no @Component in source
Derived.class.getAnnotation(Component.class) // → present! (inherited)
Derived.class.getDeclaredAnnotation(Component.class) // → null (not declared here)Así, getAnnotations() incluye las anotaciones heredadas; getDeclaredAnnotations() informa solo lo que está escrito físicamente en ese elemento. Dos límites importantes: @Inherited funciona solo para anotaciones de clase (no de métodos ni campos), y solo a lo largo de la cadena de superclase (no de interfaces).
Anotaciones repetibles
Desde Java 8, una anotación marcada con @Repeatable puede aparecer varias veces en un elemento. Internamente, el compilador agrupa las repeticiones en una anotación contenedora, por lo que getAnnotation normal no las verá — se usa getAnnotationsByType, que desempaqueta el contenedor de forma transparente:
@Repeatable(Roles.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Role { String value(); }
@Retention(RetentionPolicy.RUNTIME)
@interface Roles { Role[] value(); } // the container
@Role("admin") @Role("user") class Account { }
Account.class.getAnnotationsByType(Role.class); // → [Role(admin), Role(user)]
Account.class.getAnnotation(Role.class); // → null! (it's wrapped in Roles)Usa getAnnotationsByType(Role.class) para anotaciones repetibles; devuelve un array y maneja tanto el caso único como el repetido.
Lectura de parámetros y otros objetivos
Los parámetros obtienen sus propias anotaciones mediante el Method.getParameterAnnotations() bidimensional (un array por parámetro), o la API más limpia de Parameter:
for (Parameter p : method.getParameters()) {
if (p.isAnnotationPresent(NotNull.class)) { /* validate */ }
}Los mismos métodos de AnnotatedElement funcionan en Field, Constructor, Package e incluso en las propias anotaciones (para leer meta-anotaciones como @Retention). Para saber cómo obtener los descriptores de Method, Field y Constructor en primer lugar, consulta Reflecting Methods, Fields y Constructors.
getParameterAnnotations() devuelve un array [parameterIndex][annotation] — un array interno por parámetro, incluso para parámetros sin anotaciones (esos arrays internos simplemente están vacíos). El bucle basado en Parameter de arriba suele ser más claro.
Un ejemplo práctico: un mini escáner de validación
El programa declara anotaciones de tiempo de ejecución, marca los casos @Inherited y @Repeatable, y luego un escáner genérico recorre las anotaciones de una clase, las anotaciones de sus métodos y las anotaciones de los parámetros de sus métodos — el esqueleto de un framework de validación o enrutamiento.
Lo que se puede extraer de la ejecución:
getAnnotation(Service.class)devolvió un proxy en vivo cuyovalue()retornó"users"— el valor escrito en el código fuente. Leer una anotación es simplemente llamar a sus métodos de elemento; el framework reacciona a esos valores (aquí, tratando"users"como prefijo de ruta). La anotación lleva datos, el escáner proporciona el comportamiento.AdminControllerreportó@Servicepresente pero no declarada:isAnnotationPresentdevolviótrue(heredado deUserController) mientras quegetDeclaredAnnotationdevolviónull. Esa diferencia se debe por completo a la meta-anotación@Inherited, y funciona solo porque@Serviceapunta a una clase — los métodos y los campos nunca heredan anotaciones de esta manera.list.getAnnotation(Role.class)devolviónullaunque hay dos anotaciones@Roledirectamente en el código fuente. Las anotaciones repetibles son envueltas en un contenedorRolespor el compilador, por lo que el getter de valor único las pierde;getAnnotationsByType(Role.class)desempaquetó el contenedor y devolvió ambos roles. Usa siempregetAnnotationsByTypepara anotaciones repetibles.- Las anotaciones de parámetros eran accesibles por parámetro: el parámetro
tenantreportó@NotNullpresente ypageno. Esta granularidad por parámetro es lo que los frameworks de validación de beans y de enlace de solicitudes usan para validar o inyectar argumentos individuales. getDeclaredAnnotations()enlistcontó dos anotaciones —@Endpointy el contenedorRolessintético — confirmando que los dos@Roles se colapsaron en un único contenedor a nivel de archivo de clase. Cualquier anotación que carezca de@Retention(RUNTIME)no habría aparecido en ese recuento.