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.classet à 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 anythingLes 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— danspackage-info.java.TYPE_USE(Java 8+) — toute utilisation d'un type, y compris les casts ((@NonNull String) o), les clausesextends, 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+) — dansmodule-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 inheritanceMises 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 àFooun@Auditableprovenant 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
valuedu 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 aveccontaining 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 degetAnnotationsByType(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.
Ce qu'il faut retenir de l'exécution :
@Modulea été déclaré sur la superclasseReportingBase, mais la réflexion l'a trouvé surWeeklyReportsparce 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 :
WithInterfaceimplémenteAnnotatedInterface, qui possède@Module, maisgetAnnotation(Module.class)a retournénull.@Inheritedne remonte queextends, jamaisimplements. 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. runWeeklyportait 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@Schedulesest invisible pour le code utilisateur si vous vous en tenez àgetAnnotationsByType.- Le cas d'un seul
@SchedulesurrunDailyétait symétrique :getAnnotation(Schedule.class)a fonctionné car il n'y avait pas de conteneur, etgetAnnotationsByTypea 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. SurrunWeekly,getAnnotation(Schedule.class)a retournénull— l'annotation réelle dans le fichier class est un conteneur@Schedulessynthétisé, pas unSchedule. Atteindre le conteneur viagetAnnotation(Schedules.class)fonctionne. La règle : pour toute annotation@Repeatable, utilisez toujoursgetAnnotationsByTypeafin 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 :
- Qui doit la lire ? →
@Retention. - Où peut-elle apparaître ? →
@Target. - Les utilisateurs doivent-ils la voir dans la Javadoc ? →
@Documentedou non. - Les sous-classes doivent-elles l'hériter ? →
@Inheritedpour les marqueurs au niveau de la classe comme@Auditable. À ignorer pour les annotations au niveau des méthodes. - Doit-elle s'appliquer plus d'une fois ? →
@Repeatablesi 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.