Meta-Annotation in Java
Annotation che annotano altre annotation in Java — @Retention, @Target, @Documented, @Inherited, @Repeatable.
Una meta-annotation è un'annotation che si applica a una dichiarazione di annotation. Configura il comportamento di quell'annotation: quanto a lungo sopravvive, dove può comparire, se le sottoclassi la ereditano, se può essere applicata più di una volta. Ne esistono cinque in java.lang.annotation, e ogni tipo di annotation che scriverai ne utilizzerà almeno le prime due.
Le cinque:
@Retention— controlla se l'annotation viene mantenuta nei file.classe a runtime.@Target— limita i tipi di elementi del programma che l'annotation può decorare.@Documented— fa apparire l'annotation nel Javadoc generato.@Inherited— fa sì che le sottoclassi ereditino le annotation a livello di classe dalla loro superclasse.@Repeatable— consente di applicare la stessa annotation più di una volta allo stesso elemento.
@Retention
@Retention sceglie uno dei tre valori di 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 { }La scelta giusta dipende dal consumatore:
- Il compilatore è il consumatore (controlli di override, soppressione degli avvisi, lint) →
SOURCE. - Uno strumento di elaborazione del bytecode, un ottimizzatore JIT o un analizzatore post-compilazione è il consumatore →
CLASS. - Un framework legge l'annotation a runtime tramite reflection (DI, ORM, binding JSON) →
RUNTIME.
RUNTIME è il più permissivo — è anche il più costoso, perché ogni annotation che sopravvive aggiunge byte al file class e un piccolo overhead di reflection al momento della ricerca.
@Target
@Target limita dove l'annotation può essere posizionata. Il suo valore è un array di costanti 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 anythingI valori di ElementType che incontrerai:
TYPE— classe, interfaccia, enum, annotation.METHOD,CONSTRUCTOR,FIELD,PARAMETER,LOCAL_VARIABLE.ANNOTATION_TYPE— per le meta-annotation come quelle in questo capitolo.PACKAGE— inpackage-info.java.TYPE_USE(Java 8+) — qualsiasi uso di un tipo, inclusi i cast ((@NonNull String) o), le clausoleextends, gli argomenti generici. Utilizzato dai controllori di nullability come Checker Framework.TYPE_PARAMETER(Java 8+) — solo nelle dichiarazioni<T extends ...>.MODULE(Java 9+) — inmodule-info.java.RECORD_COMPONENT(Java 16+) — sui parametri di un'intestazione di record.
Se ometti completamente @Target, l'annotation può andare quasi ovunque — utile per marker molto generali, restrittivo per tutto il resto. Imposta quasi sempre un @Target esplicito.
@Documented
Per impostazione predefinita, le annotation non vengono mostrate nel Javadoc degli elementi che decorano. @Documented fa sì che un'annotation venga inclusa:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface ApiNote {
String value();
}Se un metodo porta @ApiNote("rate-limited to 5 rps"), Javadoc mostrerà quell'annotation nella documentazione generata. Senza @Documented, l'annotation esiste a runtime ma è invisibile nella documentazione generata. Aggiungi @Documented a tutto ciò che ti aspetti che gli utenti della tua libreria vedano.
@Inherited
@Inherited si applica solo alle annotation che hanno come target TYPE (classi). Significa: se una classe è annotata, le sue sottoclassi vengono trattate come annotate a loro volta. Il metodo getAnnotation(...) di reflection risale la catena della superclasse e la trova.
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface Auditable { }
@Auditable
class Account { }
class SavingsAccount extends Account { } // also "auditable" via inheritanceAvvertenze:
- Risale solo la catena delle superclassi — le interfacce non propagano le annotation anche con
@Inherited.class Foo implements Auditable { }non fa sì cheFooporti un@Auditabledall'interfaccia. - Influisce solo su come la reflection riporta le annotation sulle classi. Metodi, campi e parametri non ereditano mai le annotation dai membri sovrascritti.
La maggior parte dei framework preferisce ora annotation esplicite e ripetute rispetto all'ereditarietà perché le regole sono più semplici. Usa @Inherited solo quando "tutto ciò che estende una classe marker è anch'esso marcato" è genuinamente ciò che vuoi.
@Repeatable
Prima di Java 8, non era possibile applicare la stessa annotation due volte allo stesso elemento. @Repeatable rimuove questa restrizione, ma la meccanica richiede attenzione. Dichiari un'annotation contenitore che contiene un array dei valori ripetuti e punti @Repeatable al contenitore:
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() { /* ... */ }
}Regole:
- L'elemento
valuedel contenitore deve essere un array del tipo di annotation ripetuta. - Il contenitore e l'annotation ripetuta dovrebbero avere la stessa retention e almeno gli stessi target.
- Se l'annotation ripetuta è
@Documented, il contenitore deve essere@Documentedanch'esso — e lo stesso vale per@Inherited. Il compilatore rifiuta una mancata corrispondenza concontaining annotation interface ... is not @Documented. Mantieni le loro meta-annotation sincronizzate. - A runtime, il compilatore raggruppa gli usi ripetuti in una singola annotation contenitore. La reflection dispone sia di
getAnnotation(Schedule.class)(restituisce il singolo elemento del contenitore quando ce ne sono due) sia digetAnnotationsByType(Schedule.class)(restituisce direttamente l'array). Usa il secondo sulle annotation@Repeatable.
Un esempio pratico: applicare tutte e cinque le meta-annotation a una vera annotation
L'esempio costruisce un piccolo sistema di annotation dall'inizio alla fine: un @Schedule che è RUNTIME, solo sui metodi, documentato e ripetibile; un marker @Module che viene ereditato dalle sottoclassi. Il metodo main le legge poi tramite reflection.
Cosa trarre dall'esecuzione:
@Moduleera dichiarato sulla superclasseReportingBase, ma la reflection lo ha trovato suWeeklyReportsperché l'annotation porta@Inherited. La ricerca risale la gerarchia delle classi finché non trova l'annotation o esaurisce le superclassi.- Il caso dell'interfaccia ha mostrato il limite dell'ereditarietà:
WithInterfaceimplementaAnnotatedInterface, che ha@Module, magetAnnotation(Module.class)ha restituitonull.@Inheritedrisale soloextends, maiimplements. Questo mette in difficoltà molti principianti; se hai bisogno di visibilità delle annotation tra interfacce devi percorrere tu stesso l'albero dei tipi. runWeeklyportava due annotation@Schedule.getAnnotationsByType(Schedule.class)ha restituito un array di lunghezza 2 — il modo corretto per leggere le annotation ripetute. Il contenitore@Schedulesè invisibile al codice utente se si usagetAnnotationsByType.- Il caso con
@Schedulesingolo surunDailyera simmetrico:getAnnotation(Schedule.class)ha funzionato perché non c'era contenitore, egetAnnotationsByTypeha restituito un array di lunghezza 1. Entrambe le forme vanno bene quando si conosce la molteplicità. - Le righe del "caso ripetuto tramite
getAnnotation" hanno esposto la trappola. SurunWeekly,getAnnotation(Schedule.class)ha restituitonull— l'annotation effettiva nel file class è un contenitore@Schedulessintetizzato, non unSchedule. Raggiungere il contenitore tramitegetAnnotation(Schedules.class)funziona. La regola: per qualsiasi annotation@Repeatable, usa sempregetAnnotationsByTypein modo che i due casi (una occorrenza contro molte) appaiano identici.
Scegliere il proprio set
Quando scrivi una nuova annotation, decidi tutte e cinque in una volta sola:
- Chi deve leggerla? →
@Retention. - Dove può comparire? →
@Target. - Gli utenti devono vederla nel Javadoc? →
@Documentedoppure no. - Le sottoclassi devono ereditarla? →
@Inheritedper marker a livello di classe come@Auditable. Ometti per le annotation a livello di metodo. - Deve essere applicata più di una volta? →
@Repeatablese e solo se hai genuinamente bisogno della molteplicità.
Lo scheletro predefinito per un'annotation di metodo visibile a runtime:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
@interface MyAnnotation { /* elements */ }Il capitolo successivo — Annotation Personalizzate — usa esattamente questo schema per costruirne una da zero e consumarla con reflection. Per le annotation incluse nel JDK, vedi Annotation Integrate; per l'API reflection usata sopra, vedi Lettura delle Annotation con Reflection.