W3docs

Java Reflection : Lire les annotations

Lisez les métadonnées d'annotations à l'exécution en Java avec reflection — getAnnotation, getAnnotations.

Les chapitres précédents traitaient de la déclaration des annotations (voir Annotations personnalisées et Méta-annotations) ; ce chapitre concerne la lecture de ces annotations à l'exécution via la réflexion. Une annotation avec @Retention(RUNTIME) devient interrogeable sur la Class, la Method, le Field, le Constructor et le Parameter auxquels elle est attachée. La lecture des annotations est la façon dont JUnit trouve @Test, dont Spring trouve @Autowired, et dont les frameworks de validation trouvent @NotNull. Ce chapitre rassemble l'API de lecture complète en un seul endroit, y compris les subtilités liées à @Inherited et @Repeatable.

Cette page couvre les quatre méthodes de lecture d'AnnotatedElement, pourquoi la rétention est un prérequis absolu, comment @Inherited et @Repeatable modifient ce que vous récupérez, comment lire les annotations de paramètres, et un exemple de scanner de validation que vous pouvez exécuter.

Les quatre méthodes de lecture

Chaque élément annonable (Class, Method, Field, Constructor, Parameter) implémente AnnotatedElement, qui définit les mêmes quatre méthodes partout :

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

Comme l'API est uniforme, le code pour lire une annotation sur une méthode est identique à celui pour en lire une sur une classe — vous tenez simplement un AnnotatedElement différent. Les valeurs d'annotation récupérées sont des proxies générés par la JVM ; appeler a.value() est un vrai appel de méthode qui retourne la valeur de l'élément gravée lors de la compilation.

La rétention est un prérequis

Cela mérite d'être répété car c'est le bug numéro un : seules les annotations conservées avec RUNTIME sont visibles par la réflexion.

@Retention(RetentionPolicy.RUNTIME)   // <-- required for reflection
@interface Audited { String value(); }

La rétention par défaut est CLASS, ce qui conserve l'annotation dans le fichier .class mais la supprime avant l'exécution. La rétention SOURCE la supprime encore plus tôt. Si getAnnotation retourne null sur une annotation que vous voyez clairement dans le code source, l'absence de @Retention(RUNTIME) en est presque toujours la cause.

getAnnotations vs getDeclaredAnnotations et @Inherited

La différence entre ces deux méthodes tient à @Inherited. Par défaut, les annotations ne sont pas héritées par les sous-classes. Mais si un type d'annotation est lui-même méta-annoté @Inherited, alors une sous-classe hérite d'une annotation au niveau de la classe de sa superclasse :

@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)

Ainsi, getAnnotations() inclut les annotations héritées ; getDeclaredAnnotations() ne rapporte que ce qui est physiquement écrit sur cet élément. Deux limites importantes : @Inherited fonctionne uniquement pour les annotations de classe (pas pour les méthodes ou les champs), et uniquement le long de la chaîne de superclasses (pas les interfaces).

Annotations répétables

Depuis Java 8, une annotation marquée @Repeatable peut apparaître plusieurs fois sur un même élément. En coulisses, le compilateur regroupe les répétitions dans une annotation conteneur, donc le simple getAnnotation ne les verra pas — vous utilisez getAnnotationsByType, qui décompresse le conteneur de façon 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)

Utilisez getAnnotationsByType(Role.class) pour les annotations répétables ; cela retourne un tableau et gère à la fois les cas simple et répété.

Lecture des annotations de paramètres et autres cibles

Les paramètres ont leurs propres annotations via le tableau bidimensionnel Method.getParameterAnnotations() (un tableau par paramètre), ou l'API plus propre Parameter :

for (Parameter p : method.getParameters()) {
  if (p.isAnnotationPresent(NotNull.class)) { /* validate */ }
}

Les mêmes méthodes AnnotatedElement fonctionnent sur Field, Constructor, Package, et même sur les annotations elles-mêmes (pour lire les méta-annotations comme @Retention). Pour savoir comment obtenir ces handles Method, Field et Constructor, voir Réflexion sur les méthodes, les champs et les constructeurs.

Note

getParameterAnnotations() retourne un tableau [parameterIndex][annotation] — un tableau interne par paramètre, même pour les paramètres sans annotations (ces tableaux internes sont simplement vides). La boucle basée sur Parameter ci-dessus est généralement plus claire.

Exemple pratique : un mini-scanner de validation

Le programme déclare des annotations à l'exécution, marque les cas @Inherited et @Repeatable, puis un scanner générique parcourt les annotations d'une classe, les annotations de ses méthodes, et les annotations des paramètres de ses méthodes — le squelette d'un framework de validation ou de routage.

java— editable, runs on the server

Ce qu'il faut retenir de l'exécution :

  • getAnnotation(Service.class) a retourné un proxy vivant dont la méthode value() a renvoyé "users" — la valeur écrite dans le code source. Lire une annotation revient simplement à appeler ses méthodes d'élément ; le framework réagit à ces valeurs (ici, en traitant "users" comme préfixe de route). L'annotation porte des données, le scanner fournit le comportement.
  • AdminController a signalé @Service présente mais non déclarée : isAnnotationPresent a retourné true (héritée de UserController) tandis que getDeclaredAnnotation a retourné null. Cet écart est entièrement dû à la méta-annotation @Inherited, et cela fonctionne uniquement parce que @Service cible une classe — les méthodes et les champs n'héritent jamais d'annotations de cette façon.
  • list.getAnnotation(Role.class) a retourné null même si deux annotations @Role sont clairement présentes dans le code source. Les annotations répétables sont encapsulées dans un conteneur Roles par le compilateur, de sorte que le getter à valeur unique les manque ; getAnnotationsByType(Role.class) a décompressé le conteneur et retourné les deux rôles. Utilisez toujours getAnnotationsByType pour les annotations répétables.
  • Les annotations de paramètres étaient accessibles par paramètre : le paramètre tenant a signalé @NotNull présente et page non. Cette granularité par paramètre est ce que les frameworks de validation de bean et de liaison de requêtes utilisent pour valider ou injecter des arguments individuels.
  • getDeclaredAnnotations() sur list a compté deux annotations — @Endpoint et le conteneur synthétique Roles — confirmant que les deux @Role se sont consolidés en un seul conteneur au niveau du fichier de classe. Toute annotation sans @Retention(RUNTIME) n'aurait pas figuré dans ce décompte.

Pratique

Pratique
Un framework marque les méthodes avec une annotation répétable '@Role' (une méthode peut en avoir plusieurs). Sur une méthode annotée '@Role('admin') @Role('editor')', l'appel 'method.getAnnotation(Role.class)' retourne null. Pourquoi, et que faut-il appeler à la place ?
Un framework marque les méthodes avec une annotation répétable '@Role' (une méthode peut en avoir plusieurs). Sur une méthode annotée '@Role('admin') @Role('editor')', l'appel 'method.getAnnotation(Role.class)' retourne null. Pourquoi, et que faut-il appeler à la place ?
Was this page helpful?