W3docs

Les méta-annotations Java

Annotations qui annotent d'autres annotations en Java — @Retention, @Target, @Documented, @Inherited, @Repeatable.

Une méta-annotation est une annotation que vous placez sur une déclaration d'annotation. Elles configurent le comportement de cette annotation : combien de temps elle survit, où elle est autorisée à apparaître, si les sous-classes l'héritent, si vous pouvez l'appliquer plusieurs fois. Il en existe cinq dans java.lang.annotation, et chaque type d'annotation que vous écrirez utilisera au moins les deux premières.

Les cinq :

  • @Retention — contrôle si l'annotation est conservée dans les fichiers .class et à l'exécution.
  • @Target — restreint les types d'éléments de programme que l'annotation peut décorer.
  • @Documented — fait apparaître l'annotation dans la Javadoc générée.
  • @Inherited — fait hériter aux sous-classes les annotations au niveau de la classe de leur superclasse.
  • @Repeatable — permet d'appliquer la même annotation plus d'une fois au même élément.

@Retention

@Retention choisit l'une des trois valeurs de RetentionPolicy :

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.SOURCE)        // stripped by javac; never in bytecode
@interface SuppressEvenMoreWarnings { }

@Retention(RetentionPolicy.CLASS)         // in bytecode; not loaded by the VM (the default)
@interface BytecodeOnly { }

@Retention(RetentionPolicy.RUNTIME)       // in bytecode and accessible via reflection
@interface MyRuntimeMarker { }

Le bon choix dépend du consommateur :

  • Le compilateur est le consommateur (vérifications de surcharge, suppression d'avertissements, lint) → SOURCE.
  • Un outil de traitement de bytecode, un optimiseur JIT ou un analyseur post-compilation est le consommateur → CLASS.
  • Un framework lit l'annotation à l'exécution via la réflexion (DI, ORM, liaison JSON) → RUNTIME.

RUNTIME est la politique la plus permissive — c'est aussi la plus coûteuse, car chaque annotation qui survit ajoute des octets à votre fichier class et un léger surcoût de réflexion au moment de la recherche.

@Target

@Target restreint l'endroit où l'annotation peut être placée. Sa valeur est un tableau de constantes ElementType :

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@interface Audited { }                                   // only on methods or constructors

@Target(ElementType.TYPE)
@interface Entity { }                                    // only on classes/interfaces/enums

@Target({})
@interface CannotBeApplied { }                           // exists only as a type — can't be used to annotate anything

Les valeurs ElementType que vous rencontrerez :

  • TYPE — classe, interface, enum, annotation.
  • METHOD, CONSTRUCTOR, FIELD, PARAMETER, LOCAL_VARIABLE.
  • ANNOTATION_TYPE — pour les méta-annotations comme celles de ce chapitre.
  • PACKAGE — dans package-info.java.
  • TYPE_USE (Java 8+) — toute utilisation d'un type, y compris les casts ((@NonNull String) o), les clauses extends, les arguments génériques. Utilisé par les vérificateurs de nullité comme Checker Framework.
  • TYPE_PARAMETER (Java 8+) — uniquement sur les déclarations <T extends ...>.
  • MODULE (Java 9+) — dans module-info.java.
  • RECORD_COMPONENT (Java 16+) — sur les paramètres d'un en-tête de record.

Si vous omettez complètement @Target, l'annotation peut aller presque partout — utile pour les marqueurs très généraux, restrictif pour tout le reste. Définissez presque toujours un @Target explicite.

@Documented

Par défaut, les annotations ne sont pas affichées dans la Javadoc des éléments qu'elles décorent. @Documented permet à une annotation d'être incluse :

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ApiNote {
  String value();
}

Si une méthode porte @ApiNote("rate-limited to 5 rps"), la Javadoc affichera cette annotation dans la documentation générée. Sans @Documented, l'annotation existe à l'exécution mais est invisible dans la documentation générée. Ajoutez @Documented à tout ce que vous attendez que les utilisateurs de votre bibliothèque voient.

@Inherited

@Inherited s'applique uniquement aux annotations ciblant TYPE (les classes). Elle indique : si une classe est annotée, ses sous-classes sont également considérées comme annotées. La méthode getAnnotation(...) de la réflexion remontera la chaîne des superclasses pour la trouver.

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

@Auditable
class Account { }

class SavingsAccount extends Account { }                 // also "auditable" via inheritance

Mises en garde :

  • Elle ne remonte que la chaîne des superclasses — les interfaces ne propagent pas les annotations même avec @Inherited. class Foo implements Auditable { } ne fait pas porter à Foo un @Auditable provenant de l'interface.
  • Elle n'affecte que la façon dont la réflexion rapporte les annotations sur les classes. Les méthodes, champs et paramètres n'héritent jamais des annotations des membres surchargés.

La plupart des frameworks préfèrent désormais les annotations explicites et répétées à l'héritage, car les règles sont plus simples. Utilisez @Inherited uniquement lorsque "tout ce qui étend une classe marqueur est également marqué" est vraiment ce que vous souhaitez.

@Repeatable

Avant Java 8, il n'était pas possible d'appliquer deux fois la même annotation au même élément. @Repeatable lève cette restriction, mais la mécanique nécessite de l'attention. Vous déclarez une annotation conteneur qui contient un tableau des valeurs répétées, et vous pointez @Repeatable vers le conteneur :

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules { Schedule[] value(); }            // the container

@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule { String cron(); }                  // the repeated annotation

class Reports {
  @Schedule(cron = "0 9 * * MON")
  @Schedule(cron = "0 9 * * FRI")
  public void weekly() { /* ... */ }
}

Règles :

  • L'élément value du conteneur doit être un tableau du type d'annotation répété.
  • Le conteneur et l'annotation répétée doivent avoir la même rétention et au moins les mêmes cibles.
  • Si l'annotation répétée est @Documented, le conteneur doit également être @Documented — idem pour @Inherited. Le compilateur rejette une incohérence avec containing annotation interface ... is not @Documented. Gardez leurs méta-annotations synchronisées.
  • À l'exécution, le compilateur regroupe les utilisations répétées dans une seule annotation conteneur. La réflexion dispose à la fois de getAnnotation(Schedule.class) (renvoie l'unique élément du conteneur lorsqu'il y en a deux) et de getAnnotationsByType(Schedule.class) (renvoie directement le tableau). Utilisez la seconde pour les annotations @Repeatable.

Un exemple complet : appliquer les cinq méta-annotations à une vraie annotation

L'exemple construit un petit système d'annotations de bout en bout : un @Schedule qui est RUNTIME, uniquement sur les méthodes, documenté et répétable ; un marqueur @Module hérité par les sous-classes. La méthode principale les lit ensuite via la réflexion.

java— editable, runs on the server

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

  • @Module a été déclaré sur la superclasse ReportingBase, mais la réflexion l'a trouvé sur WeeklyReports parce que l'annotation porte @Inherited. La recherche remonte la hiérarchie des classes jusqu'à trouver l'annotation ou épuiser les superclasses.
  • Le cas de l'interface a montré la limite de l'héritage : WithInterface implémente AnnotatedInterface, qui possède @Module, mais getAnnotation(Module.class) a retourné null. @Inherited ne remonte que extends, jamais implements. Cela piège de nombreux débutants ; si vous avez besoin d'une visibilité des annotations à travers les interfaces, vous devez parcourir l'arbre de types vous-même.
  • runWeekly portait deux annotations @Schedule. getAnnotationsByType(Schedule.class) a retourné un tableau de longueur 2 — la bonne façon de lire les annotations répétées. Le conteneur @Schedules est invisible pour le code utilisateur si vous vous en tenez à getAnnotationsByType.
  • Le cas d'un seul @Schedule sur runDaily était symétrique : getAnnotation(Schedule.class) a fonctionné car il n'y avait pas de conteneur, et getAnnotationsByType a retourné un tableau de longueur 1. L'une ou l'autre forme convient lorsque vous connaissez la multiplicité.
  • Les lignes "repeated case via getAnnotation" ont exposé le piège. Sur runWeekly, getAnnotation(Schedule.class) a retourné null — l'annotation réelle dans le fichier class est un conteneur @Schedules synthétisé, pas un Schedule. Atteindre le conteneur via getAnnotation(Schedules.class) fonctionne. La règle : pour toute annotation @Repeatable, utilisez toujours getAnnotationsByType afin que les deux cas (une occurrence ou plusieurs) se présentent de manière identique.

Choisir votre ensemble

Lorsque vous écrivez une nouvelle annotation, décidez des cinq en même temps :

  1. Qui doit la lire ? → @Retention.
  2. Où peut-elle apparaître ? → @Target.
  3. Les utilisateurs doivent-ils la voir dans la Javadoc ? → @Documented ou non.
  4. Les sous-classes doivent-elles l'hériter ? → @Inherited pour les marqueurs au niveau de la classe comme @Auditable. À ignorer pour les annotations au niveau des méthodes.
  5. Doit-elle s'appliquer plus d'une fois ? → @Repeatable si et seulement si vous avez réellement besoin de multiplicité.

Le squelette par défaut pour une annotation de méthode visible à l'exécution :

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@interface MyAnnotation { /* elements */ }

Le prochain chapitre — Annotations personnalisées — utilise exactement ce schéma pour en construire une de zéro et la consommer avec la réflexion. Pour les annotations fournies par le JDK, voir Annotations intégrées ; pour l'API de réflexion utilisée ci-dessus, voir Lire les annotations avec la réflexion.

Pratique

Pratique
Vous déclarez `@Tagged` avec `@Inherited` et `@Target(ElementType.TYPE)`. `interface Marker {}` est annotée `@Tagged`, et `class Concrete implements Marker {}`. Que retourne `Concrete.class.getAnnotation(Tagged.class)` ?
Vous déclarez `@Tagged` avec `@Inherited` et `@Target(ElementType.TYPE)`. `interface Marker {}` est annotée `@Tagged`, et `class Concrete implements Marker {}`. Que retourne `Concrete.class.getAnnotation(Tagged.class)` ?
Was this page helpful?