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
|