Introduction aux annotations Java
Ce que sont les annotations Java, comment elles attachent des métadonnées au code et où elles sont traitées.
Une annotation est un marqueur que l'on attache à un élément Java — une classe, une méthode, un champ, un paramètre, une utilisation de type — afin d'y ajouter des métadonnées. L'annotation elle-même ne fait rien. C'est une étiquette qu'un autre morceau de code (le compilateur, un framework, un IDE, un outil de build) lit ultérieurement et exploite. @Override sur une méthode indique au compilateur « je remplace une méthode de la superclasse ; signale-moi si ce n'est pas le cas. » @Test sur une méthode indique à JUnit « c'est un test. » @Entity sur une classe indique à JPA « mappe-la vers une table de base de données. » Dans chaque cas, l'annotation ne fait que transporter de l'information ; le comportement réside dans le processeur qui la lit.
Vous avez déjà rencontré des annotations tout au long de ce livre — @Override sur les méthodes redéfinies, @FunctionalInterface sur les interfaces à méthode unique, @SuppressWarnings pour faire taire le compilateur. Cette partie du livre porte sur le système sous-jacent : ce qu'est une annotation, quand elle est disponible (source uniquement, fichier classe ou runtime), comment en écrire une, et comment les processeurs les consomment.
La forme d'une annotation
Syntaxiquement, une annotation est le symbole @ suivi du nom de l'annotation, appliqué immédiatement avant l'élément annoté :
@Override
public String toString() { ... }
@Deprecated
public void oldApi() { ... }
@SuppressWarnings("unchecked")
List<String> list = (List<String>) raw;Certaines annotations acceptent des éléments (leur équivalent de champs). Les valeurs des éléments se placent entre parenthèses :
@SuppressWarnings("unchecked") // one element, value "unchecked"
@SuppressWarnings({"unchecked", "rawtypes"}) // array of strings
@RequestMapping(path = "/users", method = GET) // two named elementsQuand une annotation déclare un seul élément appelé value, son nom peut être omis — @SuppressWarnings("unchecked") est un raccourci pour @SuppressWarnings(value = "unchecked").
Ce que les annotations ne sont pas
Trois points négatifs pour éviter les malentendus les plus fréquents :
- Les annotations n'exécutent pas de code. Écrire
@Cachedà côté d'une méthode ne met rien en cache. Il faut que quelque chose d'autre détecte@Cachedet ajoute le comportement de mise en cache. L'annotation est un drapeau, pas une fonction. - Les annotations ne sont pas des commentaires. Les commentaires disparaissent à la compilation ; les annotations sont des constructions de premier ordre du langage. Elles participent au système de types, peuvent être conservées dans le fichier classe et lues à l'exécution via la réflexion.
- Les annotations ne remplacent pas un code clair. Une longue pile d'annotations au-dessus d'une classe représente de la densité d'information, pas toujours de la clarté. Les frameworks qui s'appuient fortement sur les annotations (Spring, JPA, JAX-RS) paient la commodité d'une courbe d'apprentissage et d'un coût à l'exécution.
La durée de vie d'une annotation
Chaque annotation possède une politique de rétention qui détermine combien de temps les métadonnées survivent :
SOURCE— le compilateur la lit puis la supprime.@Overrideet@SuppressWarningsfonctionnent ainsi ; le bytecode ne conserve aucune trace d'elles.CLASS— l'annotation est écrite dans le fichier.classmais n'est pas chargée par la JVM à l'exécution. C'est la valeur par défaut. Les outils qui inspectent le bytecode (analyseurs statiques, post-processeurs) peuvent la lire.RUNTIME— l'annotation est conservée jusqu'au bout ; la réflexion peut interroger n'importe quelle classe, méthode ou champ pour obtenir ses annotations à l'exécution. Des frameworks comme Spring et Jackson s'appuient sur cela.
Vous verrez la méta-annotation @Retention(...) qui définit cette politique dans le chapitre Méta-annotations. En résumé : choisissez la rétention selon qui doit lire l'annotation — le compilateur, un outil au moment du build ou du code à l'exécution.
Où une annotation peut aller
Chaque annotation possède également une cible — l'ensemble des éléments de programme qu'elle peut légalement annoter. Les cibles courantes :
TYPE— classes, interfaces, enums.METHOD— méthodes.FIELD— champs.PARAMETER— paramètres de méthode.CONSTRUCTOR— constructeurs.LOCAL_VARIABLE— déclarations de variables locales.ANNOTATION_TYPE— autres déclarations d'annotation (méta-annotations).PACKAGE— packages, viapackage-info.java.TYPE_USE— toute utilisation d'un type, y compris les paramètres génériques et les casts (Java 8+).
Si vous placez une annotation là où son @Target ne le permet pas, le compilateur refuse. Essayer @Override sur une déclaration de classe est une erreur de compilation, car @Override cible METHOD.
Qui lit les annotations
Trois endroits consomment les données des annotations :
- Le compilateur. Les annotations intégrées comme
@Override,@SafeVarargset@FunctionalInterfacesont vérifiées parjavaclui-même. - Les processeurs d'annotations. Des outils pluggables à la compilation qui s'exécutent pendant
javac. Ils peuvent lire les annotations sur les sources compilées et générer de nouveaux fichiers sources en réponse. Lombok, Dagger, le métamodèle statique d'Hibernate et le frameworkMicronautfonctionnent ainsi. - La réflexion à l'exécution.
Method.getAnnotations(),Class.getAnnotation(...), etc. renvoient les instances d'annotation pour tout élément avec une rétentionRUNTIME. C'est ainsi que Spring décide quoi injecter, que JUnit trouve vos tests, et que Jackson mappe le JSON.
Les deux premiers n'ont besoin d'aucun support de la machine virtuelle au-delà de ce que fournit javac. Le troisième nécessite que l'annotation soit écrite dans le fichier classe et chargée par le runtime.
Exemple concret : inspecter les annotations de votre propre classe
L'objectif de cet exemple est de montrer que @Override, @Deprecated et @SuppressWarnings se ressemblent dans le code source mais se comportent différemment une fois la classe compilée. Le programme déclare une classe avec plusieurs annotations, puis demande à la réflexion ce qu'elle peut réellement voir.
Ce que l'exécution nous apprend :
- La boucle au niveau de la classe a vu
@DeprecatedsurGreetermais rien d'autre —@Deprecateda une rétentionRUNTIME.@Overrideet@SuppressWarningssontSOURCE, donc le compilateur les a supprimées avant d'écrire le fichier classe et la réflexion ne peut pas les récupérer. - La boucle par méthode n'a affiché
@Deprecatedque suroldHello. Même sitoStringétait déclarée@Overrideetcastdéclarée@SuppressWarnings("unchecked"), aucune de ces annotations n'est parvenue au fichier classe. L'information existait au moment oùjavacs'est exécuté — c'est ainsi que la vérification de redéfinition a eu lieu — puis a été supprimée. - La vérification de rétention l'a clairement montré :
@Overrideet@SuppressWarningsportent@Retention(SOURCE)dans leurs propres déclarations, tandis que@Deprecatedporte@Retention(RUNTIME). La rétention est une propriété du type d'annotation, pas de la façon dont elle est utilisée. - Lire
@DeprecatedsurGreeterviacls.getAnnotation(Deprecated.class)a renvoyé un proxy dont les méthodes d'éléments (since(),forRemoval()) retournaient les valeurs écrites dans le source. C'est l'interface d'exécution vers les métadonnées d'annotation : une instance dont les éléments sont des méthodes accesseurs. - Le message à retenir pour choisir la rétention : si le seul consommateur est
javac(vérification de redéfinition, suppression d'avertissements), utilisezSOURCE. Si un framework doit lire l'annotation pendant l'exécution du programme (DI, ORM, liaison JSON), utilisezRUNTIME. Le chapitre sur les méta-annotations explique comment le déclarer pour vos propres types d'annotation.
Ce qui vient dans cette partie
Les chapitres suivants de cette partie vous guideront à travers :
- Les annotations intégrées les plus courantes de
java.lang—@Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface. - Les méta-annotations qui configurent les vôtres —
@Retention,@Target,@Documented,@Inherited,@Repeatable. - L'écriture de types d'annotation personnalisés et leur lecture via la réflexion.
- L'API de traitement d'annotations à la compilation que les frameworks utilisent pour générer du code.
L'arc va de « ce que le langage vous donne comme annotations » à « ce que vous pouvez construire par-dessus. »