W3docs

Annotations personnalisées en Java

Définissez vos propres types d'annotations en Java, configurez leur rétention et leurs cibles, et lisez-les au moment de l'exécution.

Une annotation personnalisée est un type d'annotation que vous déclarez vous-même, plutôt qu'un type fourni par le JDK (comme @Override) ou un framework (comme @Test). La syntaxe ressemble à une interface, mais les règles sont plus strictes. Une fois déclarée, votre annotation devient un vrai type que vous pouvez attacher à du code, retrouver via la réflexion et traiter au moment de la compilation.

Ce chapitre est le guide pratique pour écrire les vôtres : le mot-clé @interface, les types d'éléments autorisés, les différences entre éléments requis et optionnels, et la façon dont un processeur lit les valeurs au moment de l'exécution. Si vous n'avez pas encore rencontré les annotations, commencez par les annotations Java et les annotations intégrées ; pour contrôler où votre annotation peut apparaître et combien de temps elle vit, consultez les méta-annotations.

La déclaration @interface

Un type d'annotation est déclaré avec le mot-clé @interface :

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audited {
  String value();                                       // required element
  String level() default "INFO";                        // element with a default
  String[] tags() default {};                           // array element with default
}

Cela déclare un nouveau type d'annotation appelé Audited dont les éléments ressemblent à des méthodes d'interface mais se comportent comme des valeurs nommées aux sites d'utilisation. Chaque « méthode » est un élément.

Utilisez-le ainsi :

@Audited("UserService.login")                            // value omitted name → "value" element
public User login(String user, String password) { ... }

@Audited(value = "Service.save", level = "WARN", tags = {"db", "write"})
public void save(Entity e) { ... }

Le raccourci value (@Audited("...") au lieu de @Audited(value = "...")) n'est disponible que lorsque l'élément est littéralement nommé value, c'est pourquoi tant d'annotations utilisent exactement ce nom pour leur paramètre principal.

Les éléments autorisés

Le corps d'un @interface est un ensemble fermé de déclarations d'éléments. Le type de retour de chaque élément doit être l'un des suivants :

  • Un primitif (int, long, double, boolean, ...).
  • String.
  • Class ou un Class<?> paramétré.
  • Un type enum.
  • Un autre type d'annotation.
  • Un tableau de l'un des types ci-dessus.

Les valeurs par défaut sont écrites avec default. La valeur par défaut doit être une constante de compilation du bon type :

@interface RetryPolicy {
  int attempts() default 3;
  long delayMs() default 100;
  Class<? extends Exception>[] on() default {Exception.class};
  Level level() default Level.WARN;
  enum Level { DEBUG, INFO, WARN, ERROR }
}

Ce que vous ne pouvez pas déclarer dans une annotation :

  • Des méthodes qui prennent des paramètres (les () sont requis mais toujours vides).
  • Des éléments génériques (<T> T value(); est illégal).
  • Des clauses throws.
  • L'héritage d'une autre interface (les annotations étendent implicitement java.lang.annotation.Annotation).
  • Des constructeurs.

Vous pouvez imbriquer des types dans une déclaration d'annotation — l'enum Level ci-dessus vit à l'intérieur de @RetryPolicy. C'est un idiome utile : il garde les options connexes dans le périmètre de l'annotation qui les utilise.

Éléments requis et optionnels

Un élément sans default est requis aux sites d'utilisation. Le compilateur échoue si vous l'oubliez :

@interface Issue { String id(); }                       // required

@Issue                                                  // compile error: missing 'id'
public void brokenLogin() { }

@Issue(id = "JIRA-123")                                 // OK: 'id' supplied
public void fixedLogin() { }

Un petit conseil de style : s'il y a une seule valeur évidente, nommez l'élément value et rendez-le requis. S'il y a plusieurs paramètres, nommez-les et donnez des valeurs par défaut sensées afin que l'appel courant reste court.

Annotations marqueurs

Une annotation sans éléments est un marqueur :

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ThreadSafe { }

Les annotations marqueurs ne transportent pas de données ; leur présence ou absence est le seul signal. La réflexion demande « cette classe a-t-elle @ThreadSafe ? » avec getAnnotation(ThreadSafe.class) != null ou isAnnotationPresent(ThreadSafe.class).

Lire les annotations au moment de l'exécution

Pour une annotation RUNTIME, la réflexion expose plusieurs méthodes sur Class, Method, Field, Constructor et Parameter (voir lire les annotations avec la réflexion pour la surface complète) :

  • isAnnotationPresent(Class) — oui/non rapide.
  • getAnnotation(Class) — retourne l'instance de l'annotation, ou null.
  • getAnnotations() — retourne toutes les annotations sur l'élément (déclarées + héritées via @Inherited).
  • getDeclaredAnnotations() — uniquement celles déclarées directement sur l'élément, en ignorant @Inherited.
  • getAnnotationsByType(Class) — gère correctement le cas @Repeatable.

La lecture a la même forme quelle que soit le type de cible avec lequel vous travaillez :

Method m = ...;
if (m.isAnnotationPresent(Audited.class)) {
  Audited a = m.getAnnotation(Audited.class);
  log(a.value(), a.level(), a.tags());
}

L'objet Audited retourné est un proxy généré par la JVM — les méthodes d'éléments (value(), level(), tags()) sont de vrais appels de méthode sur celui-ci.

Égalité, identité et toString des annotations

Les valeurs des annotations implémentent equals, hashCode et toString tels que définis par java.lang.annotation.Annotation :

  • Deux instances d'annotation sont égales lorsqu'elles sont du même type et que chaque élément compare à l'égal (avec égalité profonde pour les tableaux).
  • hashCode est dérivé des valeurs des éléments d'une manière définie.
  • toString produit un rendu stable et proche du source — utile pour la journalisation.

La réflexion retourne parfois le même proxy pour des recherches répétées sur le même élément, et parfois un nouveau. Utilisez equals, jamais ==, lors de la comparaison d'instances d'annotation.

Un exemple complet : déclarer, attacher et réfléchir

Le programme déclare deux annotations (@Audited et @Retry), les utilise sur une classe et parcourt les méthodes avec la réflexion — en exécutant chaque méthode soit dans un wrapper d'audit, soit avec une boucle de nouvelle tentative. Les annotations sont de pures métadonnées ; le comportement vit dans l'exécuteur.

java— editable, runs on the server

Ce que l'exécution nous apprend :

  • greet ne portait que @Audited, donc l'exécuteur a affiché une paire entrée/sortie autour de la méthode mais n'a pas effectué de nouvelle tentative. Le même exécuteur a géré save en détectant @Retry en plus de @Audited : la première invocation a levé une exception (saveCalls == 1), l'assistant a enregistré l'échec et bouclé, et la deuxième tentative a retourné saved: data. Les annotations elles-mêmes ne font rien — c'est l'assistant invoke qui fournit le comportement.
  • unannotated a traversé la même boucle parce que l'exécuteur est uniforme. isAnnotationPresent a retourné false pour les deux annotations, donc l'assistant n'a ni journalisé ni réessayé ; la méthode s'est simplement exécutée une fois. C'est le modèle pour les processeurs : examinez les annotations, comportez-vous de façon sensée en leur absence, ne faites jamais de cas particulier pour « c'est le chemin annoté ».
  • Chaque accesseur d'élément (a.value(), r.attempts(), r.when()) a retourné la valeur écrite dans le source. Retry.when() est revenu comme constante enum ALWAYS car le site d'appel utilisait la valeur par défaut. Les valeurs par défaut sont intégrées dans le proxy d'annotation par le compilateur ; l'appelant ne peut pas distinguer si une valeur était explicite ou par défaut.
  • Le toString de Audited a affiché une forme proche du source comme @...Audited(level="WARN", value="Service.save"). C'est une propriété de chaque proxy d'annotation — utile pour la journalisation et pour assertEquals dans les tests. (L'ordre dans lequel les éléments apparaissent dans les parenthèses n'est pas garanti et varie selon les versions du JDK, donc n'affirmez pas sur la chaîne exacte.)
  • Les deux annotations sont entièrement indépendantes au niveau du source : une méthode porte les deux à la fois et la réflexion les a retournées toutes les deux sans problème. Il n'existe pas de hiérarchie d'héritage entre les types d'annotations ; combiner des comportements s'effectue en empilant des annotations sur le même élément, pas en étendant une annotation d'une autre.

Limites

Quelques surprises courantes :

  • La rétention SOURCE ne peut pas être reflétée. Si vous oubliez @Retention(RUNTIME), la réflexion retourne silencieusement null. La valeur par défaut est CLASS, pas RUNTIME.
  • Les cibles doivent correspondre. Si @Target(METHOD) et vous mettez l'annotation sur une classe, le compilateur refuse.
  • Les valeurs par défaut des éléments doivent être des constantes de compilation. Vous ne pouvez pas mettre new ArrayList<>() par défaut ; vous pouvez mettre {} pour un tableau, une constante enum, un littéral Class ou un littéral primitif.
  • Les annotations ne peuvent pas se référencer cycliquement. Un élément de type MyAnn à l'intérieur de @interface MyAnn est rejeté.

Le chapitre suivant, le traitement des annotations, montre le côté compilation — générer de nouveaux fichiers source en réponse à vos annotations personnalisées, au lieu de (ou en plus de) les lire au moment de l'exécution.

Entraînement

Pratique
Vous déclarez `@Cached { int ttlSeconds(); }` et la placez sur une méthode. Au moment de l'exécution, `m.getAnnotation(Cached.class)` retourne `null` alors que le source a clairement `@Cached(ttlSeconds = 60)`. Quelle est la cause la plus probable ?
Vous déclarez `@Cached { int ttlSeconds(); }` et la placez sur une méthode. Au moment de l'exécution, `m.getAnnotation(Cached.class)` retourne `null` alors que le source a clairement `@Cached(ttlSeconds = 60)`. Quelle est la cause la plus probable ?
Was this page helpful?