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 hereComme 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.
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.
Ce qu'il faut retenir de l'exécution :
getAnnotation(Service.class)a retourné un proxy vivant dont la méthodevalue()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.AdminControllera signalé@Serviceprésente mais non déclarée :isAnnotationPresenta retournétrue(héritée deUserController) tandis quegetDeclaredAnnotationa retournénull. Cet écart est entièrement dû à la méta-annotation@Inherited, et cela fonctionne uniquement parce que@Servicecible une classe — les méthodes et les champs n'héritent jamais d'annotations de cette façon.list.getAnnotation(Role.class)a retournénullmême si deux annotations@Rolesont clairement présentes dans le code source. Les annotations répétables sont encapsulées dans un conteneurRolespar 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 toujoursgetAnnotationsByTypepour les annotations répétables.- Les annotations de paramètres étaient accessibles par paramètre : le paramètre
tenanta signalé@NotNullprésente etpagenon. 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()surlista compté deux annotations —@Endpointet le conteneur synthétiqueRoles— confirmant que les deux@Rolese 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.