17.7Eigene Annotationstypen *
Die in der Java-Standardbibliothek vorgegebenen Annotationen haben entweder eine besondere Semantik für den Compiler, wie @Override oder @SuppressWarnings, oder dienen zum Beispiel der Definition von Web-Services (@WebService, @WebMethod, …) oder von Komponenten mit XML-Abbildung (@XmlRootElement, @XmlElement, …). Insbesondere die Java Enterprise Edition (Java EE) macht von Annotationen fleißig Gebrauch, und es lassen sich auch neue Annotationstypen deklarieren.
17.7.1Annotationen zum Laden von Ressourcen
Im Folgenden wollen wir drei Annotationstypen deklarieren, die den Inhalt von Objektvariablen beschreiben. Zunächst werden die Annotationstypen selbst beschrieben, und abschließend folgt eine Klasse, die die Annotationen ausliest und die Ressourcen initialisiert.
Es soll möglich sein, mit @CurrentDateResource eine Objektvariable mit dem aktuellen Datum zu belegen:
public Date now;
Ist eine Variable mit @ListOfFilesResource annotiert, so sollen alle Dateien und Unterverzeichnisse aus einem gegebenen Verzeichnis aufgelistet und damit ein Feld initialisiert werden:
String[] files;
Die Annotation @UrlResource ist die komplexeste Annotation. Sie beschreibt im einfachsten Fall eine URL mit Daten von einem HTTP-Server (mit dem URL-Protokoll file:// auch vom lokalen Dateisystem), sodass eine Variable mit dem Inhalt initialisiert werden kann:
String testFile;
Der Annotation lassen sich noch einige Attribute (Element-Wert-Paare) übergeben, sodass etwa Leerraum entfernt wird oder der String in Groß-/Kleinbuchstaben konvertiert wird:
trim = true,
upperLowerCase = UpperLowerCase.UPPERCASE )
public String testFile;
Zu guter Letzt lassen sich bei @UrlResource auch beliebige Konvertierer-Klassen angeben, die den Text der Ressource transformieren:
converter = { RemoveNoWordCharactersConverter.class,
SortConverter.class } )
public String testFile;
17.7.2Neue Annotationen deklarieren
Ein Annotationstyp (engl. annotation type) wird so deklariert wie eine Schnittstelle, nur steht vor dem Schlüsselwort interface ein @-Zeichen.
Beginnen wir mit dem einfachsten Annotationstyp, CurrentDateResource:
Die Ähnlichkeit von neuen Annotationstypen und Schnittstellen ist so groß, dass in der Java Language Specification die Annotationen auch im Kapitel über Schnittstellen behandelt werden. (Später erfahren wir den Grund dafür: Der Compiler übersetzt die Annotationstypen in Schnittstellen.)
Wo sich der Annotationstyp festmachen lässt, kann eingeschränkt werden. Im Standardfall kann er überall angeheftet werden, das heißt an beliebigen Typdeklarationen, Annotationen, Aufzählungen, Objekt-/Klassenvariablen, lokalen Variablen, Parametern, Methoden, Konstruktoren oder auch an Paketen (wobei die Syntax da etwas anders ist).
Damit ist Folgendes erlaubt:
17.7.3Annotationen mit genau einem Attribut
Der Annotationstyp @CurrentDateResource kann mit keinem zusätzlichen Attribut versehen werden, da er in der bisherigen Schreibweise eine Markierungsannotation ist. Erlaubt sind zwar ein Paar runde Klammern hinter dem Namen und auch Kommentare, aber eben kein zusätzliches Attribut, wie es @ListOfFilesResource etwa wünscht:
@CurrentDateResource() public Date now;
@CurrentDateRessource( "gestern" ) public Date now; // Compilerfehler
Damit zusätzliche Informationen für den Pfadnamen bei @ListOfFilesResource("c:/") möglich sind, werden im Annotationstyp Deklarationen für Attribute eingesetzt, deren Schreibweise an Operationen einer Java-Schnittstelle erinnert. (Aber die Operationen dürfen keinen Parameter besitzen, die Rückgabe darf nicht void sein und kein throws besitzen. Und Operationen, die so heißen wie die Methoden aus Object, sind nicht zugelassen.)
Damit ein zusätzliches Attribut den Pfadnamen annehmen kann, sieht die Deklaration des Annotationstyps ListOfFilesResource so aus:
String value();
}
Damit haben wir den zweiten Annotationstyp aus unserem Beispiel vorbereitet, und gültig wäre:
String[] files;
Fehlt das erwartete Element, also der Pfad-String, gibt es einen Compilerfehler.
Attributtypen
Das, was so wie ein Rückgabetyp einer Methode aussieht, bestimmt den Typ des Attributs und ist im begrenzten Rahmen wählbar. Der Typ muss nicht immer nur String sein. Insgesamt erlaubt Java:
alle primitiven Datentypen (byte, short, int, long, float, double, boolean), aber keine Wrapper
String
Class. Insbesondere mit der generischen Angabe ermöglicht er eine präzise Klassenangabe.
Aufzählungstypen
andere Annotationen (was zu geschachtelten Annotationen führt)
Felder von oben genannten Typen. Felder von Feldern (mehrdimensionale Felder) sind aber nicht gestattet.
[»]Hinweis
Die Attribute sind typisiert, und fehlerhafte Typen lehnt der Compiler ab. null ist als Argument nie erlaubt. Mögliche Typkonvertierungen führt der Compiler automatisch durch:
@ListOfFilesResource() String[] files; // Compilerfehler
@ListOfFilesResource( null ) String[] files; // Compilerfehler
@ListOfFilesResource( 1 ) String[] files; // Compilerfehler
@ListOfFilesResource( 'C' ) String[] files; // Compilerfehler
@ListOfFilesResource( "C:" + '/' ) String[] files; // OK
17.7.4Element-Wert-Paare (Attribute) hinzufügen
Wenn der Annotationstyp ein Attribut mit dem Namen value deklariert, so muss keine Angabe über einen Schlüsselnamen gemacht werden. Möglich wäre das aber schon, und geschrieben würde das so:
String[] files;
Eine Annotation lässt sich mit einer beliebigen Anzahl an Attributen deklarieren, und das Attribut muss auch nur dann value heißen, wenn der Schlüssel nicht ausdrücklich genannt werden soll – also @ListOfFilesResource("c:/") statt @ListOfFilesResource(value = "c:/"). Ist mehr als ein Attribut nötig, muss ohnehin immer der Attributname zusammen mit der Belegung genannt werden.
Wenn @ListOfFilesResource mit einem Attribut trim ausgestattet wird, sodass die gelesenen Texte automatisch vorne und hinten den Weißraum abgeschnitten bekommen, so könnte die Deklaration des Annotationstyps so aussehen:
String value();
boolean trim();
}
Und in der Anwendung:
trim = true )
String testFile;
17.7.5Annotationsattribute vom Typ einer Aufzählung
Bisher haben wir als Attributtyp String und boolean eingesetzt. Attribute dürfen auch Aufzählungen sein. Wir wollen das für @UrlResource nutzen, damit wir beim Einlesen wählen können, ob der Text in Groß- oder Kleinbuchstaben konvertiert wird:
upperLowerCase = UpperLowerCase.UPPERCASE )
String testFile;
Für die Konvertierungsart deklarieren wir zunächst eine Aufzählung und deklarieren das Attribut upperLowerCase dann genau mit dem Aufzählungstyp:
public enum UpperLowerCase { UNCHANGED, LOWERCASE, UPPERCASE }
String value();
UpperLowerCase upperLowerCase();
}
Die Aufzählung UpperLowerCase als inneren Typ zu deklarieren, ist interessant, da sie ja nicht allgemein ist, sondern ausschließlich mit der Annotation @UrlResource Sinn ergibt.
17.7.6Felder von Annotationsattributen
Von den unterschiedlichen Elementtypen dürfen eindimensionale Felder gebildet werden. Da es keine anderen Sammlungen gibt, stellt das Feld die einzige Möglichkeit dar, beliebig viele Elemente anzugeben.
Der @UrlResource sollen beliebig viele Konvertierungsfilter zugewiesen werden. Konvertierungsfilter sind Klassen, die die Schnittstelle ResourceConverter implementieren und den eingelesenen String transformieren. Dann heißt es in der Deklaration des Annotationstyps:
String value();
Class<? extends ResourceConverter>[] converter();
}
Der interessante Teil ist natürlich Class<? extends ResourceConverter>[]. Der setzt sich wie folgt zusammen:
Da Java es nicht erlaubt, dass beliebige Attributtypen verwendet werden, bleiben bei der Angabe der Konverter nur Class-Objekte und nicht etwa ResourceConverter[].
Die Typangabe Class[] wäre nicht ausreichend, da Class mit einem generischen Typ präzisiert werden muss. Jetzt ist aber Class<ResourceConverter> auch noch nicht präzise, denn wir wollen ja nicht nur exakt den Typ RessourceConverter treffen, sondern Untertypen, also Klassen, die RessourceConverter erweitern. Damit sind wir bei Class<? extends ResourceConverter>.
Da es eine Liste von Class-Angaben werden kann, muss das Paar eckiger Klammen an die Deklaration.
Weisen wir zum Beispiel zwei Konverter – die Klassen wurden noch nicht vorgestellt, aber das folgt – der @UrlResource zu:
converter = { RemoveNoWordCharactersConverter.class,
SortConverter.class } )
public String testFile;
Bei nur einem angegebenen Konverter können die geschweiften Klammern sogar entfallen:
converter = RemoveNoWordCharactersConverter.class )
[»]Hinweis
Da ein Attribut wieder eine Annotation sein kann, ergeben sich interessante Möglichkeiten. Nehmen wir an, der Annotationstyp Name speichert Vor- und Nachnamen:
String firstname();
String lastname();
}
Ein Annotationstyp Author soll Name als Elementtyp für value nutzen:
Name[] value();
}
Vor Name steht nicht das @-Zeichen. Nur in der Anwendung:
Hätten wir das Element nicht value, sondern etwa name genannt, müsste die Angabe so heißen:
Und hätten wir mehrere Autoren angegeben, würden wir Folgendes schreiben:
{
@Name( firstname = "Christian", lastname = "Ullenboom" ),
@Name( firstname = "Hansi", lastname = "Hinterweltler" )
} )
17.7.7Vorbelegte Attribute
Im bisherigen Fall mussten alle Attributbelegungen angegeben werden, und wir konnten kein Element-Wert-Paar auslassen. Die Annotationstypen ermöglichen allerdings für Attribute Standardwerte, sodass ein Wert angeben werden kann, aber nicht muss. Statt
soll es möglich sein, trim = false wegzulasssen, weil es Standard sein soll:
Beziehungsweise dann wieder kürzer:
In der Syntax für Vorbelegungen hält dafür das Schlüsselwort default her, was auch zu einer neuen Schreibweise führt, die von den Schnittstellen abweicht.
Bei unserem @UrlResource ist nur die Angabe der Textquelle vonnöten; alles andere soll mit Default-Werten belegt sein:
enum UpperLowerCase { UNCHANGED, LOWERCASE, UPPERCASE }
String value();
boolean trim() default false;
UpperLowerCase upperLowerCase() default UpperLowerCase.UNCHANGED;
Class<? extends ResourceConverter>[] converter() default { };
}
Nachträgliche Änderung und die Sinnhaftigkeit von Standardwerten
Annotationstypen können ebenso wenig einfach geändert werden wie Schnittstellendeklarationen. Wird eine Methode nie über den Basistyp einer Schnittstelle aufgerufen, sondern die Schnittstelle lediglich implementiert, so kann diese ungenutzte Operation im Prinzip gelöscht werden. Bei Annotationen ist das genauso: Wenn ein Annotationselement mit einem Standardwert belegt ist und es nie genutzt wird, kann es gelöscht werden. Aber hier gilt analog zu den Schnittstellen: Gibt es eine dynamische Bindung über eine Schnittstelle und werden die Operationen entfernt, so gibt es genauso einen Compilerfehler, wie wenn es einen Zugriff auf ein Annotationselement gibt und es dann gelöscht wird. Auch das Ändern von Elementtypen führt im Allgemeinen zu Compilerfehlern, denn wenn aus einem int plötzlich ein String wird, fehlen Anführungszeichen.
Standardwerte sind für Annotationen ein sehr wichtiges Instrument, um neue Annotationselemente schmerzfrei einzuführen. Werden neue Elemente in bestehende Annotationstypen eingefügt, dann müssten alle existierenden konkreten Annotationen das neue Element setzen, was eine sehr große Änderung ist, vergleichbar einer neuen Operation in einer Schnittstelle. Anders als bei Schnittstellen lösen Default-Werte das Problem, da auf diese Weise für das neue Element immer gleich ein Wert vorhanden ist, der, sofern erwünscht, neu belegt werden kann. Ohne Probleme ist es möglich, einen Default-Wert hinzuzunehmen, während das Entfernen von Standardwerten wiederum kritisch ist.
17.7.8Annotieren von Annotationstypen
Drei Annotationstypen aus dem Paket java.lang haben wir schon kennengelernt. Die restlichen vier Annotationen aus dem Paket java.lang.annotation dienen dazu, Annotationstypen zu annotieren. In diesem Fall wird von Meta-Annotationen gesprochen.
Annotation | Beschreibung |
---|---|
@Target | Was lässt sich annotieren? Klasse, Methode …? |
@Retention | Wo ist die Annotation sichtbar? Nur für den Compiler oder auch für die Laufzeitumgebung? |
@Documented | Zeigt den Wunsch an, die Annotation in der Dokumentation zu erwähnen. |
@Inherited | Macht deutlich, dass ein annotiertes Element auch in der Unterklasse annotiert ist. |
@Repeatable | wenn eine Annotation mehrmals angewendet werden darf |
Tabelle 17.5Meta-Annotationen
@Target
Die Meta-Annotation @java.lang.annotation.Target beschreibt, wo eine Annotation angeheftet werden kann. Ist kein ausdrückliches @Target gewählt, gilt es für alle Elemente; die Annotation kann also etwa an Klassen stehen, aber auch an lokalen Variablen. In der Regel gibt es bei @Target ein Element, und das ist von der Aufzählung java.lang.annotation.ElementType; es deklariert die folgenden Ziele:
ElementType | Erlaubt Annotationen … |
---|---|
ANNOTATION_TYPE | nur an anderen Annotationstypen, was @Target(ANNOTATION_TYPE) somit zu einer Meta-Annotation macht |
TYPE | an allen Typdeklarationen, also Klassen, Schnittstellen, Annotationstypen, Aufzählungen |
CONSTRUCTOR | an Konstruktor-Deklarationen |
METHOD | an Deklarationen von statischen und nichtstatischen Methoden |
FIELD | an Deklarationen von statischen Variablen und Objekt-Variablen |
PARAMETER | an Parametervariablen von Methoden |
LOCAL_VARIABLE | an lokalen Variablen |
PACKAGE | an package-Deklarationen |
TYPE_PARAMETER | an der Deklaration einer Typvariablen für generische Typ-Parameter. Neu in Java 8. Wenn es etwa heißt class List<@AnAnnotation T> |
TYPE_USE | an allen Stellen, wo Typen eingesetzt werden, adressiert also Typ- Annotationen. Ebenfalls neu in Java 8. So etwas wie @NonNull (keine Annotation aus der Java SE!) ist ein Beispiel. |
Tabelle 17.6ElementType bestimmt Orte, an denen Annotationen erlaubt sind.
Soll eine Annotation etwa vor beliebigen Typen, Methoden, Paketen und Konstruktoren erlaubt sein, so setzen wir Folgendes an die Deklaration des Annotationstyps:
public @interface ...
Unsere eigenen drei Annotationstypen sind nur für Attribute sinnvoll. So nutzen wir FIELD, was hier an CurrentDateResource gezeigt wird:
public @interface CurrentDateResource { }
[»]Hinweis
Soll statt ElementType.FIELD einfach nur FIELD verwendet werden, so muss FIELD entsprechend aus ElementType statisch eingebunden werden. Damit ist folgender Programmcode eine Alternative:
import java.lang.annotation.Target;
@Target( FIELD )
public @interface CurrentDateResource { }
Mit ElementType.TYPE ist die Annotation vor allen Typen – Klassen, Schnittstellen, Annotationen, Aufzählungstypen – erlaubt. Eine Einschränkung, etwa nur auf Klassen, ist nicht möglich. Interessant ist die Tatsache, dass eine Unterteilung für Methoden und Konstruktoren möglich ist und dass sogar lokale Variablen annotiert werden können.
[zB]Beispiel
Beim existierenden Annotationstyp @Override ist die Annotation @Target schön zu erkennen:
public @interface Override
Die Idee der Meta-Annotation: Es gibt nur überschriebene Methoden.
Annotationen für Pakete sind speziell, weil sich die Frage stellt, wo hier die Metadaten über ein Paket stehen sollen. Eine Klasse selbst wird ja einem Paket zugeordnet – sollte das heißen, in irgendeiner wahllosen Typdeklaration stehen dann an der package-Deklaration die Meta-Annotationen für das Paket? Nein, denn dann würde zum einen die Annotation bei vielen Typen vielleicht nie mehr wiedergefunden, und zum anderen gäbe es bestimmt Konflikte, wenn aus Versehen an zwei Typen widersprüchliche Annotationen an der package-Deklaration stünden. Java wählt eine andere Lösung. Es muss eine Datei mit dem Namen package-info.java im jeweiligen Paket stehen, und dort darf die package-Deklaration annotiert sein. Da der Dateiname schon kein Klassenname sein kann (Minuszeichen sind nicht erlaubt), wird die Datei auch keine Typdeklaration enthalten, aber der Compiler erzeugt natürlich eine .class-Datei für die Metadaten des Pakets. Kommentare sind selbstverständlich erlaubt, und die Datei wird auch für die API-Dokumentation eines Pakets verwendet.
Dazu ein Beispiel. Ein neuer Annotationstyp AutomaticUmlDiagram soll deklariert werden, und er soll nur an Paketen gültig sein:
Listing 17.24com/tutego/insel/annotation/AutomaticUmlDiagram.java
import java.lang.annotation.*;
@Target( value = ElementType.PACKAGE )
public @interface AutomaticUmlDiagram {}
Das Paket com.tutego.insel.annotation soll nun mit AutomaticUmlDiagram annotiert werden:
Listing 17.25com/tutego/insel/annotation/package-info.java
package com.tutego.insel.annotation;
Die Datei package-info.java ist schlank, wird aber in der Regel größer sein, da sie das Javadoc des Pakets enthält.
@Retention
Die Annotation @Retention steuert, wer die Annotation sehen kann. Es gibt drei Typen, die in der Aufzählung java.lang.annotation.RetentionPolicy genannt sind:
SOURCE: Nützlich für Tools, die den Quellcode analysieren, aber die Annotationen werden vom Compiler verworfen, sodass sie nicht den Weg in den Bytecode finden.
CLASS: Die Annotationen speichert der Compiler in der Klassendatei, aber sie werden nicht in die Laufzeitumgebung gebracht.
RUNTIME: Die Annotationen werden in der Klassendatei gespeichert und sind zur Laufzeit in der JVM verfügbar.
Die Unterscheidung haben die Java-Designer vorgesehen, da nicht automatisch jede Annotation zur Laufzeit verfügbar ist (eine Begründung: andernfalls würde es den Ressourcenverbrauch erhöhen). Der Standard ist RetentionPolicy.CLASS.
[zB]Beispiel
Der Annotationstyp @Deprecated ist nur für den Compiler und nicht für die Laufzeit von Interesse:
public @interface Deprecated
Ist ein Element mit @Target annotiert, so soll diese Information auch zur Laufzeit vorliegen:
@Target( value = ANNOTATION_TYPE )
public @interface Target
Das Beispiel zeigt, dass die Anwendung auch rekursiv sein kann (natürlich auch indirekt rekursiv, denn nicht nur @Retention annotiert @Target, auch @Target annotiert @Retention).
Für den Zugriff auf die Annotationen gibt es dann, je nach Retention-Typ, unterschiedliche Varianten. Im Fall von Source ist es ein Tool, das auf Textebene arbeitet, also etwa ein Compiler oder ein statisches Analysetool, das Quellcode analysiert. Sind die Annotationen im Bytecode abgelegt, so lassen sie sich über ein Werkzeug bzw. eine Bibliothek auslesen. Drei Wege sind möglich:
Zunächst bietet Java mit der Pluggable Annotation Processing API (spezifiziert im JSR-269) eine standardisierte API zum Zugriff auf die Elemente im Quellcode. Es geht dann darum, einen Annotation Processor (eine Implementierung von der Schnittstelle javax.annotation.processing.Processor) im Compiler einzuhaken, der dann zum Beispiel Artefakte erstellen oder Fehler melden kann.
Eine andere Variante funktioniert über rohe Tools, die direkt auf der Ebene vom Bytecode arbeiten. In Frage kommen etwa Bibliotheken wie ASM (unter http://asm.ow2.org/), die alles auslesen können, was in der Klassendatei steht, also auch die Annotationen. Sie sind aber proprietär und nicht einfach zu nutzen.
Die dritte Variante ist einfach, da hier Reflection eine Möglichkeit bietet. Das schauen wir uns gleich im Anschluss an, in Abschnitt 17.7.10, »Annotierte Elemente auslesen«. Natürlich funktioniert das nur bei @Retention(RUNTIME).
@Documented
Die Annotation @Documented zeigt an, dass die Annotation in der API-Dokumentation genannt werden soll. Alle Standard-Annotationen von Java werden so angezeigt, auch @Documented selbst. In der API-Dokumentation ist für die Annotationen ein neues Segment vorgesehen.
[zB]Beispiel
@Documented ist selbst @Documented:
@Target( value = ANNOTATION_TYPE )
public @interface Documented
@Repeatable
Normalerweise nutzen Entwickler Annotationen wie einmalige Modifizierer, und es ergibt keinen Sinn, etwa @Override @Override String toString() zu schreiben, genauso wenig wie es einen Sinn ergibt, final static final double PI zu deklarieren. Doch da es durchaus Metadaten, insbesondere mit verschiedenen Werten, gibt, die mehrmals auftauchen können, bietet Java 8 eine Erweiterung, dass Annotationen wiederholt werden dürfen. Allerdings müssen die Annotationstypen dieser wiederholbaren Annotationen selbst mit einer besonderen Meta-Annotation @Repeatable ausgezeichnet werden. Damit ist es aber noch nicht getan, denn @Repeatable muss als Element einen Typ bekommen, der den Container angibt.
[zB]Beispiel
Der Annotationstyp für Autoren kann so aussehen:
Soll nun die Annotation mehrfach verwendet werden, ist die Meta-Annotation nötig und mit ihr die Angabe eines Containers:
public @interface Author { String name(); }
Der Container ist selbst ein Annotationstyp mit einem Feld als Element. Der Typ des Feldes ist exakt der wiederholbare Annotationstyp:
Autor[] value;
}
Ohne @Repeatable am Annotationstyp wird eine mehrmalige Verwendung einer Annotation zu einem Compilerfehler führen. In der Java SE 8 gibt es bisher keine Verwendung dieses Annotationstyps, also auch keine wiederholbaren Annotationen in der Standardbibliothek.
Erfragt werden wiederholbare Annotationen durch Methoden der Schnittstelle java.lang.reflect.AnnotatedElement (und das ist AccessibleObject, Class, Constructor, Executable, Field, Method, Package, Parameter):
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)
17.7.9Deklarationen für unsere Ressourcen-Annotationen
Da unsere drei Annotationen zur Laufzeit ausgelesen werden sollen, muss die @Retention mit RetentionPolicy.RUNTIME gesetzt sein. Damit sind unsere Annotationstypen vollständig, und der Quellcode soll an dieser Stelle aufgeführt werden.
Der einfachste Annotationstyp war CurrentDateResource:
Listing 17.26com/tutego/insel/annotation/CurrentDateResource.java, CurrentDateResource
@Target( ElementType.FIELD )
@Retention( RetentionPolicy.RUNTIME )
public @interface CurrentDateResource { }
Der Annotationstyp ListOfFilesResource erwartet eine Pfadangabe, ist aber nicht deutlich komplexer als CurrentDateResource:
Listing 17.27com/tutego/insel/annotation/ListOfFilesResource.java, ListOfFilesResource
@Target( ElementType.FIELD )
@Retention( RetentionPolicy.RUNTIME )
public @interface ListOfFilesResource {
String value();
}
Und zu guter Letzt: Der Annotationstyp UrlResource hat am meisten zu bieten. Doch beginnen wir zunächst mit der Deklaration der Schnittstelle für die Konverter:
Listing 17.28com/tutego/insel/annotation/ResourceConverter.java, ResourceConverter
String convert( String input );
}
Zwei Implementierungen sollen für das Beispiel genügen:
Listing 17.29com/tutego/insel/annotation/SortConverter.java, SortConverter Teil 1
@Override public String convert( String input ) {
return input.replaceAll( "\\W", "" );
}
}
Listing 17.30com/tutego/insel/annotation/SortConverter.java, SortConverter Teil 2
@Override public String convert( String input ) {
char[] chars = input.toCharArray();
Arrays.sort( chars );
return new String( chars );
}
}
Damit kann dann der letzte Annotationstyp übersetzt werden:
Listing 17.31com/tutego/insel/annotation/UrlResource.java, UrlResource
@Target( ElementType.FIELD )
@Retention( RetentionPolicy.RUNTIME )
public @interface UrlResource {
enum UpperLowerCase { UNCHANGED, LOWERCASE, UPPERCASE }
String value();
boolean trim() default false;
UpperLowerCase upperLowerCase() default UpperLowerCase.UNCHANGED;
Class<? extends ResourceConverter>[] converter() default { };
}
17.7.10Annotierte Elemente auslesen
Ob eine Klasse annotiert ist, erfragt ganz einfach die Methode isAnnotationPresent(Class<? extends Annotation> annotationClass) auf dem Class-Objekt:
Listing 17.32com/tutego/insel/annotation/CheckIsStringBufferInputStreamDeprecated.java, main()
println( StringBufferInputStream.class.isAnnotationPresent( Deprecated.class ) ); // true
Schnittstelle AnnotatedElement
Da unterschiedliche Dinge annotierbar sind, schreibt eine Schnittstelle AnnotatedElement für die Klassen Class, Constructor, Field, Method, Package und AccessibleObject folgende Operationen vor:
Annotation[] getAnnotations()
Liefert die an dem Element festgemachten Annotationen. Gibt es keine Annotation, ist das Feld leer. Die Methode liefert auch Annotationen, die aus den Obertypen kommen.Annotation[] getDeclaredAnnotations()
Liefert die Annotationen, die exakt an diesem Element festgemacht sind, sprich, vererbte Annotationen zählen nicht dazu.<T extends Annotation> T getAnnotation(Class<T> annotationType)
Liefert die Annotation für einen bestimmten Typ. Ist keine Annotation vorhanden, ist die Rückgabe null. Der generische Typ ist bei der Rückgabe hilfreich. Denn das Argument ist ein Class-Objekt, das den Annotationstyp repräsentiert. Die Rückgabe ist genau die konkrete Annotation für das annotierte Element. Betrachtet auch Annotationen, die aus den Obertypen kommen.boolean isAnnotationPresent(Class<? extends Annotation> annotationType)
Gibt es die angegebene Annotation? Betrachtet auch geerbte Annotationen. Im Grunde getAnnotation(annotationType) != null.default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
Wie getAnnotation(Class<T>), nur ohne geerbte Annotation.default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)
Liefert ein Array von Annotationen vom gewünschten Typ; ein Array ist nötig bei wiederholten Annotationen. Neu in Java 8. Einmal inklusive/exklusive geerbter Annotationen.
Um die Annotationen etwa von Variablen oder Methoden zu erfragen, ist ein wenig Reflection-Wissen nötig. Ist obj ein Objekt, so findet folgende Schleife alle mit CurrentDateResource annotierten Objektvariablen und gibt eine Meldung aus:
if ( field.isAnnotationPresent( CurrentDateResource.class ) )
System.out.println( "CurrentDateResource gesetzt" );
AnnotatedType
Vier Methoden kommen in Java 8 in Executable hinzu:
extends AccessibleObject
implements Member, GenericDeclaration
abstract AnnotatedType getAnnotatedReturnType()
AnnotatedType getAnnotatedReceiverType()
AnnotatedType[] getAnnotatedParameterTypes()
AnnotatedType[] getAnnotatedExceptionTypes()
Die Schnittstelle AnnotatedType (erweitert AnnotatedElement) ist ebenfalls neu in Java 8 und repräsentiert einen annotierten Typ. Constructor überschreibt Executable die Methode getAnnotatedReturnType(), da Konstruktoren aber keine Rückgabe haben, ist das Ergebnis der Typ des erzeugten Objekts.
Zudem hat Field seit Java 8 eine Methode getAnnotatedType(), die ebenso AnnotatedType als Rückgabe hat.
Class besitzt seit Java 8 zwei neue Methoden:
AnnotatedType[] getAnnotatedInterfaces()
AnnotatedType getAnnotatedSuperclass()
17.7.11Auf die Annotationsattribute zugreifen
Um auf die einzelnen Attribute einer Annotation zuzugreifen, müssen wir etwas mehr über die Umsetzung einer Annotation vom Compiler und der JVM wissen. Übersetzt der Compiler einen Annotationstyp, generiert er daraus eine Schnittstelle.
[zB]Beispiel
Für den Annotationstyp ListOfFilesResource generiert der Compiler:
public interface ListOfFilesResource extends Annotation {
public abstract String value();
}
Rufen wir auf einem AnnotatedElement, etwa Field, eine Methode wie getAnnotations() oder getAnnotation(Class<T> annotationClass) auf, bekommen wir ein Objekt, das Zugriff auf unsere Element-Wert-Paare liefert. Denn zur Laufzeit werden über java.lang.reflect.Proxy Objekte gebaut, die unsere Schnittstelle – das ist ListOfFilesResource – implementiert und so die Methode value() anbietet.
[»]Hinweis
Die Annotation ist zur Laufzeit ein Proxy-Objekt, und daher kann der Annotationstyp keine eigene Klasse erweitern und auch keine anderen eigenen Schnittstellen implementieren. Ein Annotationstyp kann auch keine anderen Annotationstypen erweitern. Es könnte eine eigene Klasse zwar die Schnittstelle java.lang.annotation.Annotation implementieren, doch entsteht dadurch keine echte Annotation, was den Versuch sinnlos macht.
Testen wir die Möglichkeit, indem wir zwei annotierte Variablen in eine Klasse setzen und dann per Reflection über alle Variablen laufen und alle Annotationen erfragen lassen:
Listing 17.33com/tutego/insel/annotation/GetTheUrlResourceValues.java, GetTheUrlResourceValues
@UrlResource( value = "http://tutego.de/javabuch/aufgaben/bond.txt",
upperLowerCase = UpperLowerCase.UPPERCASE, trim = true,
converter = { RemoveNoWordCharactersConverter.class,
SortConverter.class } )
public String testFile;
@XmlValue @Deprecated
public String xmlValue;
public static void main( String[] args ) throws Exception {
for ( Field field : GetTheUrlResourceValues.class.getFields() )
for ( Annotation a : field.getAnnotations() )
System.out.println( a );
}
}
Die Ausgabe zeigt drei Annotationen:
RemoveNoWordCharactersConverter, class com.tutego.insel.annotation.SortConverter], ¿
upperLowerCase=UPPERCASE, value=http://tutego.de/javabuch/aufgaben/bond.txt)
@javax.xml.bind.annotation.XmlValue()
@java.lang.Deprecated()
Die Default-Werte werden zur Laufzeit gesetzt.
17.7.12Komplettbeispiel zum Initialisieren von Ressourcen
Zusammenfassend können wir jetzt eine Klasse vorstellen, die tatsächlich die mit den Ressourcen-Annotationen versehenen Variablen mit sinnvollem Inhalt füllt. Zunächst betrachten wir ein Beispiel, das die Nutzung einer solchen Klasse aufzeigt.
Die Klasse Resources bildet den Rahmen für Objekte, die automatisch aufgebaut und korrekt initialisiert werden sollen:
Listing 17.34com/tutego/insel/annotation/AnnotatedResourceExample.java, Resources
@CurrentDateResource()
public Date now;
@ListOfFilesResource( value = "c:/" )
public String[] files;
@UrlResource( "http://tutego.de/javabuch/aufgaben/bond.txt" )
public String testFile;
}
Einer zweiten Klasse geben wir ein main(String[]) und setzen dort die Aufforderung, ein Objekt vom Typ Resources anzulegen und zu initialisieren:
Listing 17.35com/tutego/insel/annotation/AnnotatedResourceExample.java, AnnotatedResourceExample
public static void main( String[] ars ) {
Resources resources =
ResourceReader.getInitializedInstance( Resources.class );
System.out.println( resources.now );
System.out.println( Arrays.toString( resources.files ) );
System.out.println( resources.testFile );
}
}
Kommen wir zum Herzen, der Klasse ResourceReader:
Listing 17.36com/tutego/insel/annotation/ResourceReader.java
import java.io.File;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.Date;
import java.util.Scanner;
public class ResourceReader {
public static <T> T getInitializeInstace( Class<T> ressources ) {
try {
T obj = ressources.newInstance();
for ( Field field : ressources.getFields() ) {
if ( field.isAnnotationPresent( CurrentDateResource.class ) )
field.set( obj, new Date() );
else if ( field.isAnnotationPresent( ListOfFilesResource.class ) )
field.set( obj, new File(field.getAnnotation(
ListOfFilesResource.class ).value().toString()).list() );
else if ( field.isAnnotationPresent( UrlResource.class ) ) {
String url = field.getAnnotation( UrlResource.class ).value();
try ( Scanner scanner = new Scanner( new URL( url ).openStream() ) ) {
String content = scanner.useDelimiter( "\\Z" ).next();
if ( field.getAnnotation( UrlResource.class ).trim() )
content = content.trim();
switch ( field.getAnnotation( UrlResource.class ).upperLowerCase() ) {
case UPPERCASE: content = content.toUpperCase(); break;
case LOWERCASE: content = content.toLowerCase(); break;
default: // Nichts zu tun
}
Class<? extends ResourceConverter>[] converterClasses =
field.getAnnotation( UrlResource.class ).converter();
for ( Class<? extends ResourceConverter> converterClass : converterClasses )
content = converterClass.newInstance().convert( content );
field.set( obj, content );
}
}
}
return obj;
}
catch ( Exception e ) { // Ignoriere alle Ausnahmen
return null;
}
}
}
An den folgenden Anweisungen ist das Prinzip gut ablesbar:
for ( Field field : ressources.getFields() )
if ( field.isAnnotationPresent( CurrentDateResource.class ) )
field.set( newInstance, new Date() );
Zunächst wird ein neues Exemplar, ein Behälter, aufgebaut. Dann läuft eine Schleife über alle Variablen. Gibt es zum Beispiel die Annotation CurrentDateResource an einer Variablen, so wird ein Date-Objekt aufgebaut und mit set(…) die Variable mit dem Datum initialisiert.
17.7.13Mögliche Nachteile von Annotationen
Annotationen sind eine gewaltige Neuerung und sicherlich die wichtigste seit vielen Java-Jahren. Auch wenn die Generics auf den ersten Blick bedeutsam erscheinen, sind die Annotationen ein ganz neuer Schritt in die deklarative Programmierung, wie sie Frameworks schon heute aufzeigen. Völlig problemlos sind Annotationen allerdings nicht, und so müssen wir etwas Wasser in den Wein gießen:
Die Annotationen sind stark mit dem Quellcode verbunden, können also auch nur dort geändert werden. Ist der Original-Quellcode nicht verfügbar, etwa weil der Auftraggeber ihn geschlossen hält, ist eine Änderung der Werte nahezu unmöglich.
Wenn Annotationen allerdings nach der Übersetzung nicht mehr geändert werden können, stellt das bei externen Konfigurationsdateien kein Problem dar. Externe Konfigurationsdateien können ebenso den Vorteil bieten, dass die relevanten Informationen auf einen Blick erfassbar sind und sich mitunter nicht redundant auf unterschiedliche Java-Klassen verteilen.
Klassen mit Annotationen sind invasiv und binden auch die Implementierungen an einen gewissen Typ, wie es Schnittstellen tun. Sind die Annotationstypen nicht im Klassenpfad, kommt es zu einem Compilerfehler.
Bisher gibt es keine Vererbung von Annotationen: Ein Annotationstyp kann keinen anderen Annotationstyp erweitern.
Die bei den Annotationen gesetzten Werte lassen sich zur Laufzeit erfragen, aber nicht modifizieren.
Warum werden Annotationen mit @interface deklariert, einer Schreibweise, die in Java sonst völlig unbekannt ist?
Ein Problem gibt es allerdings nur bei finalen statischen Variablen (Konstanten), das bei den Default-Werten der Annotationen nicht vorkommt: Weil die Default-Werte zur Laufzeit gesetzt werden, lassen sie sich in der Deklaration vom Annotationstyp leicht ändern, und eine Neuübersetzung des Projekts kann somit unterbleiben.
Zur Ehrenrettung sei erwähnt, dass moderne Frameworks wie JPA oder JSF 2 aus dem Java EE-Standard immer noch den Einsatz von XML vorsehen. So lässt sich auf Annotationen verzichten bzw. XML einsetzen, sodass Zuweisungen aus den Annotationen überschrieben werden können.