Introduzione alle Annotazioni Java
Cosa sono le annotazioni Java, come aggiungono metadati al codice e dove vengono elaborate.
Un'annotazione è un marcatore che si applica a un elemento del sorgente Java — una classe, un metodo, un campo, un parametro, un uso di tipo — per aggiungere metadati. L'annotazione in sé non fa nulla. È un'etichetta che qualche altro pezzo di codice (il compilatore, un framework, un IDE, uno strumento di build) legge successivamente e risponde. @Override su un metodo dice al compilatore "sto sovrascrivendo un metodo della superclasse; avvertimi se non è così." @Test su un metodo dice a JUnit "questo è un test." @Entity su una classe dice a JPA "mappa questa su una tabella del database." In ogni caso l'annotazione porta solo informazioni; il comportamento vive nel processore che la legge.
Hai già visto le annotazioni in tutto questo libro — @Override sui metodi sovrascritti, @FunctionalInterface sulle interfacce con un solo metodo, @SuppressWarnings per silenziare il compilatore. Questa parte del libro riguarda il sistema sottostante: cosa è un'annotazione, quando è disponibile (solo sorgente, file di classe o runtime), come scriverne una propria e come i processori la consumano.
La forma di un'annotazione
Sintatticamente, un'annotazione è il simbolo @ seguito dal nome dell'annotazione, applicata immediatamente prima dell'elemento che annota:
@Override
public String toString() { ... }
@Deprecated
public void oldApi() { ... }
@SuppressWarnings("unchecked")
List<String> list = (List<String>) raw;Alcune annotazioni accettano elementi (la loro versione dei campi). I valori degli elementi vanno tra parentesi:
@SuppressWarnings("unchecked") // one element, value "unchecked"
@SuppressWarnings({"unchecked", "rawtypes"}) // array of strings
@RequestMapping(path = "/users", method = GET) // two named elementsSe un'annotazione dichiara un singolo elemento chiamato value, puoi omettere il nome — @SuppressWarnings("unchecked") è un'abbreviazione di @SuppressWarnings(value = "unchecked").
Cosa le annotazioni non sono
Tre negazioni che evitano i malintesi più comuni:
- Le annotazioni non eseguono codice. Scrivere
@Cachedvicino a un metodo non mette nulla in cache. Qualcos'altro deve cercare@Cachede aggiungere il comportamento di caching. L'annotazione è un flag, non una funzione. - Le annotazioni non sono commenti. I commenti scompaiono al momento della compilazione; le annotazioni sono costrutti del linguaggio di prima classe. Partecipano al sistema dei tipi, possono essere richieste per vivere nel file di classe e possono essere lette a runtime via reflection.
- Le annotazioni non sono un sostituto per un codice chiaro. Una lunga serie di annotazioni sopra una classe è densità di informazioni, non sempre chiarezza. I framework che si affidano pesantemente alle annotazioni (Spring, JPA, JAX-RS) pagano per la comodità con una curva di apprendimento e un costo a runtime.
Quando vive l'annotazione
Ogni annotazione ha una politica di retention che decide quanto a lungo i metadati sopravvivono:
SOURCE— il compilatore la legge e poi la scarta.@Overridee@SuppressWarningsfunzionano così; il bytecode non contiene alcuna traccia di loro.CLASS— l'annotazione viene scritta nel file.classma non caricata dalla JVM a runtime. Questo è il valore predefinito. Gli strumenti che ispezionano il bytecode (analizzatori statici, post-processori) possono leggerla.RUNTIME— l'annotazione viene mantenuta fino alla fine; la reflection può chiedere a qualsiasi classe, metodo o campo le sue annotazioni a runtime. Framework come Spring e Jackson si affidano a questo.
Vedrai la meta-annotazione @Retention(...) che imposta questa politica nel capitolo sulle Meta-annotazioni. In breve: scegli la retention in base a chi ha bisogno di leggere l'annotazione — il compilatore, uno strumento a tempo di build o il codice a runtime.
Dove può andare l'annotazione
Ogni annotazione ha anche un target — l'insieme degli elementi del programma che può annotare legalmente. I target comuni:
TYPE— classi, interfacce, enum.METHOD— metodi.FIELD— campi.PARAMETER— parametri di metodo.CONSTRUCTOR— costruttori.LOCAL_VARIABLE— dichiarazioni di variabili locali.ANNOTATION_TYPE— altre dichiarazioni di annotazioni (meta-annotazioni).PACKAGE— package, tramitepackage-info.java.TYPE_USE— qualsiasi uso di un tipo, inclusi i parametri generici e i cast (Java 8+).
Se inserisci un'annotazione dove il suo @Target non lo consente, il compilatore rifiuta. Provare @Override su una dichiarazione di classe è un errore di compilazione perché @Override ha come target METHOD.
Chi legge le annotazioni
Tre posti consumano i dati delle annotazioni:
- Il compilatore. Le annotazioni integrate come
@Override,@SafeVarargse@FunctionalInterfacevengono controllate dajavacstesso. - I processori di annotazioni. Strumenti collegabili a tempo di compilazione che vengono eseguiti durante
javac. Possono leggere le annotazioni sui sorgenti in compilazione e generare nuovi file sorgente in risposta. Lombok, Dagger, il metamodello statico di Hibernate e il frameworkMicronautfunzionano tutti in questo modo. - La reflection a runtime.
Method.getAnnotations(),Class.getAnnotation(...), ecc. restituiscono le istanze di annotazione per qualsiasi elemento con retentionRUNTIME. È così che Spring decide cosa iniettare, come JUnit trova i tuoi test e come Jackson mappa il JSON.
I primi due non richiedono supporto della macchina virtuale oltre a quello fornito da javac. Il terzo richiede che l'annotazione sia scritta nel file di classe e caricata dal runtime.
Un esempio pratico: ispezionare le annotazioni sulla propria classe
Lo scopo di questo esempio è mostrare che @Override, @Deprecated e @SuppressWarnings sembrano identici nel sorgente ma si comportano diversamente una volta compilata la classe. Il programma dichiara una classe con diverse annotazioni, poi chiede alla reflection cosa riesce effettivamente a vedere.
Cosa ricavare dall'esecuzione:
- Il ciclo a livello di classe ha visto
@DeprecatedsuGreeterma nient'altro —@Deprecatedha retentionRUNTIME.@Overridee@SuppressWarningssonoSOURCE, quindi il compilatore li ha eliminati prima che il file di classe venisse scritto e la reflection non può recuperarli. - Il ciclo per metodo ha stampato
@Deprecatedsolo suoldHello. Anche setoStringera dichiarato@Overrideecastera dichiarato@SuppressWarnings("unchecked"), nessuna delle due annotazioni è arrivata al file di classe. L'informazione esisteva nel momento in cuijavacgirava — è così che è avvenuto il controllo dell'override — e poi è stata scartata. - Il controllo della retention lo ha chiarito:
@Overridee@SuppressWarningsportano@Retention(SOURCE)nelle proprie dichiarazioni, mentre@Deprecatedporta@Retention(RUNTIME). La retention è una proprietà del tipo di annotazione, non di come viene usata. - Leggere
@DeprecatedsuGreetertramitecls.getAnnotation(Deprecated.class)ha restituito un proxy i cui metodi elemento (since(),forRemoval()) hanno restituito i valori scritti nel sorgente. Questa è l'interfaccia runtime per i metadati delle annotazioni: un'istanza i cui elementi sono metodi accessor. - La conclusione per scegliere la retention: se l'unico consumatore è
javac(controlli di override, soppressione degli avvisi), usaSOURCE. Se un framework ha bisogno di leggere l'annotazione mentre il programma è in esecuzione (DI, ORM, binding JSON), usaRUNTIME. Il capitolo sulle meta-annotazioni spiega come dichiarare questo per i tuoi tipi di annotazione personalizzati.
Cosa verrà in questa parte
I capitoli rimanenti in questa parte ti guideranno attraverso:
- Le annotazioni integrate più comuni in
java.lang—@Override,@Deprecated,@SuppressWarnings,@SafeVarargs,@FunctionalInterface. - Le meta-annotazioni che configurano le tue —
@Retention,@Target,@Documented,@Inherited,@Repeatable. - Scrivere tipi di annotazione personalizzati e leggerli via reflection.
- L'API di elaborazione delle annotazioni a tempo di compilazione che i framework usano per generare codice.
Il percorso va da "quali annotazioni il linguaggio ti fornisce" a "quali annotazioni puoi costruire su di esse."