W3docs

Annotazioni personalizzate in Java

Definisci i tuoi tipi di annotazione in Java, configura retention e target, e leggili a runtime con la reflection.

Un'annotazione personalizzata è un tipo di annotazione che dichiari tu stesso, anziché uno fornito dal JDK (come @Override) o da un framework (come @Test). La sintassi assomiglia a un'interfaccia, ma le regole sono più rigide. Una volta dichiarata, la tua annotazione diventa un tipo reale che puoi allegare al codice, cercare tramite reflection e processare in fase di compilazione.

Questo capitolo è la guida pratica per scrivere le proprie annotazioni: la parola chiave @interface, quali tipi di elementi sono ammessi, come differiscono gli elementi obbligatori da quelli opzionali, e come un processore legge i valori a runtime. Se non hai ancora incontrato le annotazioni, inizia con annotazioni Java e le annotazioni predefinite; per controllare dove può apparire la tua annotazione e per quanto tempo vive, consulta le meta-annotazioni.

La dichiarazione @interface

Un tipo di annotazione si dichiara con la parola chiave @interface:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audited {
  String value();                                       // required element
  String level() default "INFO";                        // element with a default
  String[] tags() default {};                           // array element with default
}

Questo dichiara un nuovo tipo di annotazione chiamato Audited i cui elementi sembrano metodi di un'interfaccia ma si comportano come valori con nome nei siti d'uso. Ogni "metodo" è un elemento.

Usalo così:

@Audited("UserService.login")                            // value omitted name → "value" element
public User login(String user, String password) { ... }

@Audited(value = "Service.save", level = "WARN", tags = {"db", "write"})
public void save(Entity e) { ... }

La scorciatoia value (@Audited("...") invece di @Audited(value = "...")) è disponibile solo quando l'elemento si chiama letteralmente value, motivo per cui così tante annotazioni usano esattamente quel nome per il loro parametro principale.

Quali elementi sono ammessi

Il corpo di un @interface è un insieme chiuso di dichiarazioni di elementi. Il tipo di ritorno di ciascun elemento deve essere uno dei seguenti:

  • Un tipo primitivo (int, long, double, boolean, ...).
  • String.
  • Class o un Class<?> parametrizzato.
  • Un tipo enum.
  • Un altro tipo di annotazione.
  • Un array di uno qualsiasi dei tipi precedenti.

I valori predefiniti si scrivono con default. Il valore predefinito deve essere una costante a tempo di compilazione del tipo corretto:

@interface RetryPolicy {
  int attempts() default 3;
  long delayMs() default 100;
  Class<? extends Exception>[] on() default {Exception.class};
  Level level() default Level.WARN;
  enum Level { DEBUG, INFO, WARN, ERROR }
}

Cosa non puoi dichiarare in un'annotazione:

  • Metodi che accettano parametri (le () sono obbligatorie ma sempre vuote).
  • Elementi generici (<T> T value(); non è valido).
  • Clausole throws.
  • Ereditarietà da un'altra interfaccia (le annotazioni estendono implicitamente java.lang.annotation.Annotation).
  • Costruttori.

Puoi annidare tipi all'interno di una dichiarazione di annotazione — l'enum Level sopra vive all'interno di @RetryPolicy. È un idioma utile: mantiene le opzioni correlate nell'ambito dell'annotazione che le usa.

Elementi obbligatori e facoltativi

Un elemento senza default è obbligatorio nei siti d'uso. Il compilatore fallisce se lo dimentichi:

@interface Issue { String id(); }                       // required

@Issue                                                  // compile error: missing 'id'
public void brokenLogin() { }

@Issue(id = "JIRA-123")                                 // OK: 'id' supplied
public void fixedLogin() { }

Una piccola nota di stile: se c'è un unico valore ovvio, nomina l'elemento value e rendilo obbligatorio. Se ci sono più parametri, dagli un nome e assegna valori predefiniti sensati così la chiamata comune rimane breve.

Annotazioni marcatore

Un'annotazione senza elementi è un marcatore:

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

Le annotazioni marcatore non trasportano dati; la loro presenza o assenza è l'unico segnale. La reflection chiede "questa classe ha @ThreadSafe?" con getAnnotation(ThreadSafe.class) != null oppure isAnnotationPresent(ThreadSafe.class).

Leggere le annotazioni a runtime

Per un'annotazione RUNTIME, la reflection espone diversi metodi su Class, Method, Field, Constructor e Parameter (consulta leggere le annotazioni con la reflection per la superficie completa):

  • isAnnotationPresent(Class) — risposta rapida sì/no.
  • getAnnotation(Class) — restituisce l'istanza dell'annotazione, o null.
  • getAnnotations() — restituisce tutte le annotazioni sull'elemento (dichiarate + ereditate tramite @Inherited).
  • getDeclaredAnnotations() — solo quelle dichiarate direttamente sull'elemento, ignorando @Inherited.
  • getAnnotationsByType(Class) — gestisce correttamente il caso @Repeatable.

La lettura ha la stessa forma indipendentemente dal tipo di target su cui stai lavorando:

Method m = ...;
if (m.isAnnotationPresent(Audited.class)) {
  Audited a = m.getAnnotation(Audited.class);
  log(a.value(), a.level(), a.tags());
}

L'Audited restituito è un proxy generato dalla JVM — i metodi degli elementi (value(), level(), tags()) sono chiamate di metodo reali su di esso.

Uguaglianza, identità e toString delle annotazioni

I valori delle annotazioni implementano equals, hashCode e toString come definito da java.lang.annotation.Annotation:

  • Due istanze di annotazione sono uguali quando sono dello stesso tipo e ogni elemento risulta uguale (con deep-equality per gli array).
  • hashCode è derivato dai valori degli elementi in modo definito.
  • toString produce una rappresentazione stabile, simile al sorgente — utile per il logging.

La reflection a volte restituisce lo stesso proxy per ricerche ripetute sullo stesso elemento, e a volte ne restituisce uno nuovo. Usa equals, mai ==, quando confronti istanze di annotazione.

Un esempio completo: definire, allegare e riflettere

Il programma dichiara due annotazioni (@Audited e @Retry), le usa su una classe e percorre i metodi con la reflection — eseguendo ciascun metodo all'interno di un wrapper di auditing o con un ciclo di retry. Le annotazioni sono puri metadati; il comportamento vive nell'executor.

java— editable, runs on the server

Cosa apprendere dall'esecuzione:

  • greet aveva solo @Audited, quindi l'executor ha stampato una coppia enter/exit attorno al metodo senza riprovare. Lo stesso executor ha gestito save rilevando @Retry in aggiunta a @Audited: la prima invocazione ha lanciato un'eccezione (saveCalls == 1), il metodo helper ha registrato il fallimento e ha ripetuto il ciclo, e il secondo tentativo ha restituito saved: data. Le annotazioni di per sé non hanno fatto nulla — è l'helper invoke ad aver fornito il comportamento.
  • unannotated è passato attraverso lo stesso ciclo perché l'executor è uniforme. isAnnotationPresent ha restituito false per entrambe le annotazioni, quindi l'helper non ha né registrato né riprovato; il metodo è stato semplicemente eseguito una volta. Questo è il pattern per i processori: esaminare le annotazioni, comportarsi in modo sensato quando sono assenti, senza mai trattare specialmente "questo è il percorso annotato."
  • Ogni accessor di elemento (a.value(), r.attempts(), r.when()) ha restituito il valore scritto nel sorgente. Retry.when() ha restituito la costante enum ALWAYS perché il sito di chiamata ha usato il default. I valori predefiniti sono incorporati nel proxy dell'annotazione dal compilatore; il chiamante non può distinguere se un valore era esplicito o predefinito.
  • Il toString di Audited ha stampato una forma simile al sorgente come @...Audited(level="WARN", value="Service.save"). Questa è una proprietà di ogni proxy di annotazione — utile per il logging e per assertEquals nei test. (L'ordine in cui gli elementi appaiono all'interno delle parentesi non è garantito e varia tra le versioni del JDK, quindi non fare asserzioni sulla stringa esatta.)
  • Le due annotazioni sono del tutto indipendenti a livello di sorgente: un metodo le porta entrambe contemporaneamente e la reflection le ha restituite entrambe senza problemi. Non esiste una gerarchia di ereditarietà tra tipi di annotazione; la combinazione di comportamenti si ottiene impilando annotazioni sullo stesso elemento, non estendendo un'annotazione da un'altra.

Dove questo smette di funzionare

Alcune sorprese comuni:

  • La retention SOURCE non può essere riflessa. Se dimentichi @Retention(RUNTIME), la reflection restituisce silenziosamente null. Il valore predefinito è CLASS, non RUNTIME.
  • I target devono corrispondere. Se @Target(METHOD) e metti l'annotazione su una classe, il compilatore rifiuta.
  • I valori predefiniti degli elementi devono essere costanti a tempo di compilazione. Non puoi impostare come default new ArrayList<>(); puoi usare come default {} per un array, una costante enum, un letterale Class, o un letterale primitivo.
  • Le annotazioni non possono fare riferimento a se stesse in modo ciclico. Un elemento di tipo MyAnn all'interno di @interface MyAnn viene rifiutato.

Il prossimo capitolo, annotation processing, mostra il lato della compilazione — generare nuovi file sorgente in risposta alle tue annotazioni personalizzate, invece di (o in aggiunta a) leggerli a runtime.

Pratica

Pratica
Dichiari `@Cached { int ttlSeconds(); }` e la metti su un metodo. A runtime `m.getAnnotation(Cached.class)` restituisce `null` anche se nel sorgente c'è chiaramente `@Cached(ttlSeconds = 60)`. Qual è la causa più probabile?
Dichiari `@Cached { int ttlSeconds(); }` e la metti su un metodo. A runtime `m.getAnnotation(Cached.class)` restituisce `null` anche se nel sorgente c'è chiaramente `@Cached(ttlSeconds = 60)`. Qual è la causa più probabile?
Was this page helpful?