MokaByte 94 - Marzo 2005 
Java 5
IV parte: le annotazioni
di
Andrea Gini
Questo mese ci occuperemo di una nuova funzionalità di Java 5 che permette di espandere la gamma dei modificatori Java con delle apposite annotazioni create ad hoc. Come vedremo si tratta di un costrutto simile per certi versi alle interfacce, non particolarmente difficile da usare, ma che per la sua natura è riservato a pochi casi concreti d'utilizzo. Si invita il programmatore a leggere questo articolo senza particolare apprensione, dal momento che quanto scritto tornerà di certo utile per conoscenza, ma non verrà probabilmente mai usato, per ragioni che verranno spiegate.

Il problema delle annotazioni
Molte API Java richiedono la realizzazione di una grossa quantità di codice ad hoc, che nonostante segua un preciso standard descritto da rigorose specifiche, deve per forza essere adattato ad ogni particolare caso, anche se con minime varianti. Alcuni esempi sono i JavaBeans, che richiedono la realizzazione di un'apposita classe BeanInfo, e gli Enterprise JavaBeans, che richiedono un deployment descriptor. Queste classi di appoggio potrebbero essere ricavate automaticamente a partire dalla classe principale, se solo questa contenesse le informazioni necessarie. Le annotazioni sono una infrastruttura (in inglese "facility") che permette di risolvere questo problema: grazie alle annotazioni è possibile decorare il codice con informazioni (annotazioni) simili ai modificatori (public, private, protected) che, processate da un opportuno tool, permettono, tra le altre cose, di automatizzare la produzione di codice di appoggio.

 

Guardare e non toccare
Quanto detto nel precedente paragrafo dovrebbe aver già chiarito un primo importante punto: il normale programmatore non dovrebbe interessarsi più di tanto alla creazione di tipi di annotazione, cosa che sicuramente verrà fatta dagli sviluppatori della prossima generazione di ambienti di sviluppo (JBuilder, Eclipse e così via). D'altra parte, è importante imparare la sintassi delle annotazioni, dal momento che simili tools potrebbero ricorrere ad esse per aggiungere informazioni che il programmatore ha inserito attraverso appositi strumenti grafici. Pertanto, è importante imparare a riconoscerle e, qualora se ne incontrasse nel codice, lasciarle dove stanno per evitare di produrre danni irreparabili.

Per il resto le annotazioni non influenzano direttamente la semantica di un programma: le informazioni che contengono sono riservate ad appositi tool (simili ai vecchi pre-processori di codice presenti ad esempio nel C e nel C++) che useranno questi dati nei modi più svariati. D'altra parte le annotazioni possono essere lette a runtime, e per questa ragione non è escluso che in certi casi possano intervenire, anche se indirettamente, nella semantica di un programma.

 

Dichiarazione di tipi di annotazione
Come già precisato, il normale programmatore non dovrebbe aver a che fare con la definizione di tipi di annotazioni. Tuttavia va precisato che la cosa di per se non è particolarmente complessa, e che somiglia alla definizione di interfaccia. Un simbolo @ precede la parola chiave interface; seguono le dichiarazioni di metodo, che denotano gli elementi del tipo di annotazione:

package annotation;

public @interface FutureChanges {
String description();
String programmer() default "[unassigned]";
DaysOfWeeks[] availableDays();
}

La dichiarazione di un'annotazione deve essere salvata in un file con lo stesso nome ed estensione .java, e viene compilata in un comune file .class, eventualmente in un particolare package, usando la direttiva package come si farebbe in una qualsiasi classe. Anche la import può essere utilizzata come si farebbe in una classe. Le dichiarazioni di metodo non possono avere parametri o lanciare eccezioni; inoltre i tipi di ritorno possono essere solo di tipo primitivo, String, Class, enum, annotazione o un array di uno qualsiasi dei tipi precedenti. Si noti invece che i metodi possono avere valori di default, come si può vedere nel precedente esempio.

 

Uso delle annotazioni
Una volta che un tipo di annotazione è stato definito, è possibile usare tali annotazioni per decorare le dichiarazioni di un programma, come si farebbe con i modificatori:

@ FutureChanges (description = "Add Functionality", programmer = "Mario Rossi",availableDays = {DaysOfWeek.MONDAY, DaysOfWeek.FRIDAY})
public void doSomething() { ... }

Per convenzione, le annotazioni precedono gli altri modificatori. Un'annotazione consiste in un simbolo @ seguito dal tipo di annotazione a cui si fa riferimento e una lista di coppie elemento-valore separate da una virgola e messe tra parentesi. Nel caso un'annotazione richieda un vettore di valori, questi vanno messi tra parentesi graffe, separati da virgole, come si vede nel terzo valore dell'annotazione precedente. I valori possono essere solamente costanti: non è possibile assegnare ad un valore una variabile del programma.

 

Marcatori
I marcatori (marker annotation type) sono un tipi di annotazioni prive di parametri:

public @interface Test { }

Quando si utilizzano i marcatori nel codice, non è necessario ricorrere alle parentesi:

@Test public void method1() { ... }
Annotazioni con un singolo elemento
Nelle annotazioni con un singolo elemento, quest'ultimo viene generalmente chiamato "value":

/**
* Associates a copyright notice with the annotated API element.
*/
public @interface Copyright {
String value();
}

Questa convenzione permette di omettere il nome dell'elemento e il simbolo "=" in un'annotazione di questo tipo:

@Copyright("2002 Yoyodyne Propulsion Systems")
public class OscillationOverthruster { ... }


Meta-annotazioni
Le meta-annotazioni sono annotazioni di annotazioni. Per quanto sembri un gioco di parole, è perfettamente logico pensare che le annotazioni, in qualità di elementi di un programma Java, possano a loro volta possedere degli appositi modificatori: il package java.lang.annotation contiene al suo interno le seguenti definizioni di annotazione:

  • Documented: Serve a specificare che il corrispondente tipo di annotazione deve essere documentato da javadoc
  • Inherited: I tipi di annotazione che contengono questa meta annotazione possono essere usati solo come annotazione di classe: servono a specificare che il tipo di annotazione corrispondente viene ereditato da tutte le sottoclassi.
  • Retention: seve ad indicare quanto a lungo un tipo di annotazione vada trattenuto. A differenza delle meta-annotazioni precedenti, Retention richiede un parametro. Il package java.lang.annotation contiene l'enum RetentionPolicy che specifica i possibili valori di tale parametro:
    • SOURCE: le annotazioni presenti nel sorgente vengono scartate dal compilatore, per cui non sono disponibili solo per tool di elaborazione di codice che precedono la compilazione.
    • CLASS: le annotazioni vengono registrate nel file .class dal compilatore, ma non sono accessibili a runtime.
    • RUNTIME: le annotazioni vengono registrate nel file .class dal compilatore, e sono disponibili per la lettura a runtime attraverso la reflection.
  • Target: indica il tipo di elemento al quale l'annotazione può essere applicata. Anche in questo caso è richiesto un parametro, che può essere scelto tra quelli disponibili nella enum ElementType:
    • ANNOTATION_TYPE
    • CONSTRUCTOR
    • FIELD
    • LOCAL_VARIABLE
    • METHOD
    • PACKAGE
    • PARAMETER
    • TYPE

    Si noti che, oltre a classi metodi e attributi, è possible dichiarare tipi di annotazione che operano su variabili locali, su package, su parametri e su ulteriori tipi di annotazioni.

Il frammento di codice seguente presenta un esempio di uso di meta-annotazioni:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test { }

In questo esempio l'annotazione Test viene dichiarata come annotazione di metodo, disponibile per la lettura a runtime tramite la reflection.

 

Uso della reflection
L'interfaccia AnnotatedElement, che viene realizzata dalle classi Class, Constructor, Field, Method e Package, dispone di una serie di nuovi metodi che permettono di interrogare un oggetto in modo da prelevarne le annotazioni, a patto che la meta annotazione @Retention sia posta a RetentionPolicy.RUNTIME.

  • <A extends Annotation> A getAnnotation(Class<A> annotationClass) : data una classe, un'interfaccia o un tipo di annotazione, questo metodo restituisce l'annotazione del tipo specificato dal tipo parametrico <A>, se presente, altrimenti null.
  • Annotation[] getAnnotations(): restituisce tutte le annotazioni presenti nell'elemento. Se non sono presenti annotazioni viene restituito un vettore di lunghezza 0.
  • Annotation[] getDeclaredAnnotations(): Restituisce tutte le annotazioni presenti nel presente elemento, escluse le annotazioni ereditate. Anche in questo caso, se non sono presenti annotazioni viene restituito un vettore di lunghezza 0.
  • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass): Verifcia se l'elemento in questione contiene un'annotazione del tipo specificato dal parametro.

Esempio riassuntivo
Per riassumere quanto visto fino ad ora è opportuno presentare un esempio riassuntivo completo. In primo luogo si definisce un tipo di annotazione valido per il test:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}

Quindi un programma-giocattolo con le dichiarazioni di metodo annotate:

public class TestClass {
@Test
public void m1() {}

@Test
public void m2() {}

public class InnerClass1 {
@Test
public void m3() {}
}
public class InnerClass2 {
@Test
public void m4() {}
}
}

Infine un programma che attraverso la reflection esplora la classe indicata come parametro in cerca di annotazioni:

import java.lang.reflect.*;
import java.lang.annotation.*;

public class AnnotationVisitor {

private int spaces = 0;

protected void indent() {
spaces++;
}
protected void deindent() {
spaces --;
}
protected void print(String s) {
for(int i=0 ; i<spaces ; i++)
System.out.print(" ");
System.out.println(s);
}
public void visit(Class c) {
print("Class "+c.getName());
indent();
Field[] fields = c.getFields();
for(Field f : fields)
visit(f);
Constructor[] constructors = c.getConstructors();
for(Constructor constructor : constructors)
visit(constructor);
Method[] methods = c.getMethods();
for(Method m : methods)
visit(m);
Class[] classes = c.getClasses();
for(Class cl : classes)
visit(cl);

Annotation[] annotations = c.getAnnotations();
for(Annotation a : annotations)
print("Class "+c.getName()+" contains annotation "+a);
deindent();
}
public void visit(Constructor c) {
print("Constructor "+c.getName());
indent();
Annotation[] annotations = c.getAnnotations();
for(Annotation a : annotations)
print("Constructor "+c.getName()+" contains annotation "+a);
deindent();
}
public void visit(Field f) {
print("Field "+f.getName());
indent();
Annotation[] annotations = f.getAnnotations();
for(Annotation a : annotations)
print("Field "+f.getName()+" contains annotation "+a);
deindent();
}
public void visit(Method m) {
print("Method "+m.getName());
indent();
Annotation[] annotations = m.getAnnotations();
for(Annotation a : annotations)
print("Method "+m.getName()+" contains annotation "+a);
deindent();
}
public void visit(Package p) {
print("Package "+p.getName());
indent();
Annotation[] annotations = p.getAnnotations();
for(Annotation a : annotations)
print("Package "+p.getName()+" contains annotation "+a);
deindent();
}

public static void main(String argv[]) throws Exception {
if(argv.length!=1)
System.out.println("Usage: java AnnotationInspector <classname>");
else {
Class c = Class.forName(argv[0]);
AnnotationInspector a = new AnnotationInspector();
a.visit(c);
}
}
}

Il programma usa una variante del Pattern Visitor (cfr [1]) per esplorare tutti gli elementi di una classe. Oltre ai metodi visit(), tipici del Pattern Visitor, si notino i metodi di appoggio indent(), deindent() e print(String s) che permettono di dare una struttura all'output del programma. Per ogni elemento (campo, metodo, costruttore e così via) stampa il nome e di seguito, indentate di un paio di spazi, tutti i sotto-elementi e le eventuali annotazioni.

Dopo aver compilato l'annotazione e le due classi, si può avviare il programma in questo modo:

java AnnotationVisitor TestClass

In risposta si otterrà un output come il seguente:

Class TestClass
Constructor TestClass
Method m1
Method m1 contains annotation @Test()
Method m2
Method m2 contains annotation @Test()
Method hashCode
Method getClass
Method wait
Method wait
Method wait
Method equals
Method notify
Method notifyAll
Method toString
Class TestClass$InnerClass2
Constructor TestClass$InnerClass2
Method m4
Method m4 contains annotation @Test()
Method hashCode
Method getClass
Method wait
Method wait
Method wait
Method equals
Method notify
Method notifyAll
Method toString
Class TestClass$InnerClass1
Constructor TestClass$InnerClass1
Method m3
Method m3 contains annotation @Test()
Method hashCode
Method getClass
Method wait
Method wait
Method wait
Method equals
Method notify
Method notifyAll
Method toString

Si noti che questo output riporta anche tutti i metodi e i campi presenti nella classe Object, di cui la classe TestClass è sottoclasse. Si provi ad utilizzare il precedente programma su classi del JSDK, tipo java.lang.String o altre, in modo da rilevare le annotazioni presenti nelle classi di sistema (tipo @java.lang.Deprecated, che ora è una vera e propria annotazione, e non solo un tag Javadoc).

 

Conclusioni
In questo articolo sono state esaminate le annotazioni. In principio sono state definite le ragioni che stanno dietro a questo costrutto, quindi sono stati viste le varie modalità per definire tipi di annotazione. Infine si è visto come interrogare a runtime le annotazioni. Con questo articolo termina la rassegna sulle principali novità introdotte con Java 5: un sentito ringraziamento a quanti hanno seguito con interesse questa rubrica.

 

Bibliografia
[1] Design Patterns
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides
ed. Addison-Wesley Professional Computing Series

 

Risorse
Scarica l'esempio mostrato nell'articolo

MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it