W3docs

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 here

Como 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.

Nota

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.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • getAnnotation(Service.class) devolvió un proxy en vivo cuyo value() 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.
  • AdminController reportó @Service presente pero no declarada: isAnnotationPresent devolvió true (heredado de UserController) mientras que getDeclaredAnnotation devolvió null. Esa diferencia se debe por completo a la meta-anotación @Inherited, y funciona solo porque @Service apunta a una clase — los métodos y los campos nunca heredan anotaciones de esta manera.
  • list.getAnnotation(Role.class) devolvió null aunque hay dos anotaciones @Role directamente en el código fuente. Las anotaciones repetibles son envueltas en un contenedor Roles por el compilador, por lo que el getter de valor único las pierde; getAnnotationsByType(Role.class) desempaquetó el contenedor y devolvió ambos roles. Usa siempre getAnnotationsByType para anotaciones repetibles.
  • Las anotaciones de parámetros eran accesibles por parámetro: el parámetro tenant reportó @NotNull presente y page no. 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() en list contó dos anotaciones — @Endpoint y el contenedor Roles sinté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.

Práctica

Práctica
Un framework marca métodos con una anotación repetible '@Role' (un método puede tener varias). En un método anotado '@Role('admin') @Role('editor')', llamar a 'method.getAnnotation(Role.class)' devuelve null. ¿Por qué, y qué debería llamarse en su lugar?
Un framework marca métodos con una anotación repetible '@Role' (un método puede tener varias). En un método anotado '@Role('admin') @Role('editor')', llamar a 'method.getAnnotation(Role.class)' devuelve null. ¿Por qué, y qué debería llamarse en su lugar?
Was this page helpful?