Java Reflection: Lettura delle Annotazioni
Leggi i metadati delle annotazioni a runtime in Java con la reflection — getAnnotation, getAnnotations.
I capitoli precedenti hanno trattato la dichiarazione delle annotazioni (vedi Annotazioni Personalizzate e Meta-Annotazioni); questo capitolo riguarda la lettura delle annotazioni a runtime tramite la reflection. Un'annotazione con @Retention(RUNTIME) diventa interrogabile su Class, Method, Field, Constructor e Parameter a cui è associata. La lettura delle annotazioni è il modo in cui JUnit trova @Test, Spring trova @Autowired e i framework di validazione trovano @NotNull. Questo capitolo raccoglie in un unico posto l'intero API di lettura, incluse le particolarità di @Inherited e @Repeatable.
Questa pagina copre i quattro metodi di lettura di AnnotatedElement, perché la retention è un prerequisito fondamentale, come @Inherited e @Repeatable modificano ciò che si ottiene, come leggere le annotazioni dei parametri e un esempio funzionante di scanner di validazione che puoi eseguire.
I quattro metodi di lettura
Ogni elemento annotabile (Class, Method, Field, Constructor, Parameter) implementa AnnotatedElement, che definisce gli stessi quattro metodi ovunque:
AnnotatedElement el = SomeClass.class; // or a Method, Field, etc.
el.isAnnotationPresent(Audited.class); // boolean — quick check
el.getAnnotation(Audited.class); // the annotation instance, or null
el.getAnnotations(); // ALL annotations (declared + inherited)
el.getDeclaredAnnotations(); // only those declared directly herePoiché l'API è uniforme, il codice per leggere un'annotazione da un metodo è identico a quello per leggerla da una classe — si tiene semplicemente un AnnotatedElement diverso. I valori delle annotazioni recuperati sono proxy generati dalla JVM; chiamare a.value() è una vera chiamata a metodo che restituisce il valore dell'elemento definito al momento della compilazione.
La retention è un prerequisito
Vale la pena ripeterlo perché è il bug numero uno: solo le annotazioni con retention RUNTIME sono visibili alla reflection.
@Retention(RetentionPolicy.RUNTIME) // <-- required for reflection
@interface Audited { String value(); }La retention predefinita è CLASS, che mantiene l'annotazione nel file .class ma la elimina prima del runtime. La retention SOURCE la elimina ancora prima. Se getAnnotation restituisce null su un'annotazione che puoi vedere chiaramente nel sorgente, la mancanza di @Retention(RUNTIME) è quasi sempre la causa.
getAnnotations vs getDeclaredAnnotations e @Inherited
La differenza tra questi due è @Inherited. Per impostazione predefinita le annotazioni non vengono ereditate dalle sottoclassi. Ma se un tipo di annotazione è esso stesso meta-annotato con @Inherited, allora una sottoclasse eredita un'annotazione a livello di classe dalla sua superclasse:
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Component { }
@Component class Base { }
class Derived extends Base { } // Derived has no @Component in source
Derived.class.getAnnotation(Component.class) // → present! (inherited)
Derived.class.getDeclaredAnnotation(Component.class) // → null (not declared here)Quindi getAnnotations() include le annotazioni ereditate; getDeclaredAnnotations() riporta solo ciò che è fisicamente scritto su quell'elemento. Due limiti importanti: @Inherited funziona solo per le annotazioni di classe (non per metodi o campi), e solo lungo la catena della superclasse (non delle interfacce).
Annotazioni ripetibili
Da Java 8, un'annotazione contrassegnata con @Repeatable può apparire più volte sullo stesso elemento. Internamente il compilatore raggruppa le ripetizioni in un'annotazione contenitore, quindi il semplice getAnnotation non le vedrà — si usa getAnnotationsByType, che spacchetta in modo trasparente il contenitore:
@Repeatable(Roles.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Role { String value(); }
@Retention(RetentionPolicy.RUNTIME)
@interface Roles { Role[] value(); } // the container
@Role("admin") @Role("user") class Account { }
Account.class.getAnnotationsByType(Role.class); // → [Role(admin), Role(user)]
Account.class.getAnnotation(Role.class); // → null! (it's wrapped in Roles)Usa getAnnotationsByType(Role.class) per le annotazioni ripetibili; restituisce un array e gestisce sia il caso singolo che quello ripetuto.
Lettura delle annotazioni di parametri e altri target
I parametri ricevono le proprie annotazioni tramite l'array bidimensionale Method.getParameterAnnotations() (un array per parametro), oppure tramite l'API più chiara di Parameter:
for (Parameter p : method.getParameters()) {
if (p.isAnnotationPresent(NotNull.class)) { /* validate */ }
}Gli stessi metodi di AnnotatedElement funzionano su Field, Constructor, Package, e persino sulle annotazioni stesse (per leggere le meta-annotazioni come @Retention). Per come ottenere gli handle di Method, Field e Constructor, vedi Reflecting Methods, Fields e Constructors.
getParameterAnnotations() restituisce un array [parameterIndex][annotation] — un array interno per ogni parametro, anche per i parametri senza annotazioni (quegli array interni sono semplicemente vuoti). Il ciclo basato su Parameter sopra è di solito più chiaro.
Un esempio pratico: un mini scanner di validazione
Il programma dichiara annotazioni a runtime, contrassegna i casi @Inherited e @Repeatable, e poi uno scanner generico percorre le annotazioni di una classe, le annotazioni dei suoi metodi e le annotazioni dei parametri dei suoi metodi — lo scheletro di un framework di validazione o routing.
Cosa osservare dall'esecuzione:
getAnnotation(Service.class)ha restituito un proxy live il cuivalue()ha dato"users"— il valore scritto nel sorgente. Leggere un'annotazione significa semplicemente chiamare i metodi dei suoi elementi; il framework reagisce a quei valori (qui, trattando"users"come prefisso di route). L'annotazione porta dati, lo scanner fornisce il comportamento.AdminControllerha riportato@Servicecome presente ma non dichiarata:isAnnotationPresentha restituitotrue(ereditata daUserController) mentregetDeclaredAnnotationha restituitonull. Questa differenza è interamente dovuta alla meta-annotazione@Inherited, e funziona solo perché@Servicepunta a una classe — i metodi e i campi non ereditano mai le annotazioni in questo modo.list.getAnnotation(Role.class)ha restituitonullanche se due annotazioni@Rolesono chiaramente presenti nel sorgente. Le annotazioni ripetibili vengono raggruppate in un contenitoreRolesdal compilatore, quindi il getter a valore singolo non le vede;getAnnotationsByType(Role.class)ha spacchettato il contenitore e restituito entrambi i ruoli. Usa sempregetAnnotationsByTypeper le annotazioni ripetibili.- Le annotazioni dei parametri erano accessibili per singolo parametro: il parametro
tenantha riportato@NotNullpresente epageno. Questa granularità per parametro è ciò che i framework di bean-validation e request-binding usano per validare o iniettare i singoli argomenti. getDeclaredAnnotations()sulistha contato due annotazioni —@Endpointe il contenitore sinteticoRoles— confermando che i due@Rolesi sono collassati in un unico contenitore a livello di class file. Qualsiasi annotazione priva di@Retention(RUNTIME)non sarebbe apparsa in quel conteggio.