Deep-Dive into Annotations – Teil 3

JAVAPRO - Deep-Dive into Annotations

Java-Annotationen sind ein mächtiges Sprachmerkmal, deren Interna allgemein nicht sonderlich bekannt sind. In Teil 3 unserer dreiteiligen Serie geht es um die Auswertung eigener Annotationen zur Laufzeit.

In Teil 1 unserer Serie wurden diverse Verwendungszwecke für Annotationen aufgezeigt und die fünf Java-SE-Standardannotationen detailliert diskutiert. Teil 2 führte aus, wie eigene Annotationstypen programmiert werden können. Dabei wurden insbesondere die zahlreichen Optionen bei der Konfiguration sowie diverse Besonderheiten betrachtet. Die Syntax der Definition eigener Annotationen ist wahrlich sehr speziell und passt so gar nicht ins Bild der ansonsten eigentlich schönen und konsistenten Java-Syntax.

Im hier vorliegenden dritten und letzten Teil kommen wir zur Krönung des Einsatzes von Annotationen. Die Deklaration und das Anbringen von Annotationen macht nur Sinn, wenn sich diese auch wieder auslesen lassen. Erinnern wir uns, dass es sich gemäss offizieller Definition bei Anntationen lediglich um Marker handelt, die Informationen mit einem Programmkonstrukt verknüpfen, aber keinen Effekt zur Laufzeit haben. Annotationen können also von sich selbst aus nichts „machen“. Daraus ergeben sich drei Schlussfolgerungen:

  1. Zum Auslesen von Annotationen liegt der Einsatz von Reflection auf der Hand.
  2. Es lassen sich einzig die Existenz bzw. Nichtexistenz von Annotationen feststellen und die in den Annotationen enthaltenen Parameter auslesen. Mit anderen Worten, Annotationen lassen sich nicht ausführen.
  3. Soll das Vorhandensein einer Annotation mit irgendwelchen Aktionen verknüpft werden, so hat dies von aussen zu geschehen. Sämtliche Programmlogik muss in normalem Java-Code in diesen „Erkennungsroutinen“ hinterlegt sein.

 

Die Optionen zur Definition eigener Annotationen sind zahlreich. Dementsprechend zahlreich sind auch die verschiedenen Arten, wie sich Annotationen in eigenen Programmen auslesen lassen. Dazu werden wir nachfolgend alle typischen Fälle betrachten, wie sich welche Art von Annotationen auslesen lässt.  

 

Deklaration verschiedener Annotationstypen

Um die unterschiedlichen Arten zum Auslesen von Annotationen aufzeigen zu können, benötigen wir zuerst verschiedene eigene Annotationstypen. Folgende Annotationen stellen eine Zusammenfassung der in Teil 2 erläuterten Optionen dar:

Listing 1

  • Um zur Laufzeit Zugriff auf die Annotationen zu haben, müssen diese allesamt mit @Retention(RetentionPolicy.RUNTIME) versehen werden. RetentionPolicy.SOURCE wird wohl nur für eigene Compiler oder Tools, interessant sein, die direkt auf Source-Code-Ebene arbeiten, zum Beispiel so etwas wie Javadoc. RetentionPolicy.CLASS funktioniert entsprechend nur für eigene Tools, die direkt mit dem Bytecode der Klassen arbeiten, zum Beispiel Code-Obfuscatoren oder allenfalls eigene Klassenlader. Das Gros der Anwendungsfälle wird das Auslesen der Annotationen zur Laufzeit ausmachen, und dafür kommt nur RetentionPolicy.RUNTIME in Frage.
  • Aus Gründen der Einfachheit wurden die meisten Annotationstypen mit @Target(ElementType.METHOD) versehen, lassen sich also nur zur Annotation von Methoden verwenden. Dies ist keinesfalls einschränkend zu verstehen. Das Annotieren zum Beispiel von Klassen (ElementType.TYPE), Attributen (ElementType.FIELD) oder Methoden-Parametern (ElementType.PARAMETER) wäre in den meisten Fällen analog möglich, sofern die passenden @Target konfiguriert und die entsprechenden Reflection-Aufrufe vorgenommen werden. Um den Rahmen nicht zu sprengen, wurde darauf in diesem Artikel verzichtet.
  • NoValueAnnotation ist eine leere Marker-Annotation. SingleValueAnnotation ist eine solche mit einem einzigen Wert, in diesem Fall einem Integer mit dem Standardnamen value. Wir erinnern uns: Der Name value muss bei der Anwendung einer Annotation nicht explizit genannt werden, alle anderen Namen hingegen schon.
  • MultiValueAnnotation ist eine Annotation mit mehreren Werten gleichen Typs und dem Standardnamen value, das heisst, auch hier können bei der Anwendung die Werte einfach in geschweiften Klammern mitgegeben werden, ohne dass value explizit dazugeschrieben werden muss. RepeatableValueAnnotation ist ebenfalls in der Lage, mehrere Werte gleichen Typs aufzunehmen. Im Gegensatz zu MultiValueAnnotation muss dies aber mit mehreren Einzelaufrufen geschehen. Um dies zu erreichen, wurde die Annotation wiederum mit @Repeatable annotiert, einer neuen Meta-Annotation seit Java 8. @Repeatable funktioniert nur in Verbindung mit einem Container, der hier @RepeatableValueAnnotationContainer genannt wurde und nicht zum direkten Einsatz als Annotation gedacht ist, sondern seinen Dienst nur hinter den Kulissen verrichten soll.
  • SingleNamedValueAnnotation und MultiNamedValueAnnotation sind Annotationen mit einem respektive mehreren Werten, die jedoch nicht standardmässig value, sondern individuell benannt sind (name respektive first, second und third).
  • EnumValueAnnotation zeigt die Deklaration und später den Einsatz einer Annotation, die ihren eigenen Aufzählungstyp enthält (in diesem Beispiel GreekLetter).
  • NonInheritedAnnotation und InheritedAnnotation unterscheiden sich einzig durch die @Inherited Meta-Annotation. Zur Erinnerung: @Inherited funktioniert nur bei Klassen (daher hier auch das @Target(ElementType.TYPE)), nicht bei Interfaces und erst recht nicht bei Methoden, Attributen oder dergleichen.
  • ParameterAnnotation annotiert, wie der Name bereits verrät, Methodenparameter, um abschliessend aufzeigen zu können, wie zum Beispiel die Argumente einer Methode abgefragt werden können.

 

Anbringung der Annotationen

Das Anbringen der im vorherigen Abschnitt beschriebenen Annotationen erklärt sich nun von selbst.

Listing 2

Praktisch alle Annotationen werden auf oder in der Klasse Super angewendet. Ganz unten im Quellcode findet sich noch eine Klasse Sub, die von Super erbt und später das Verhalten der @Inherited Klassenannotation demonstrieren soll. Im folgenden Abschnitt geht es darum, die verschiedenen Arten in eigenen Programmen abzufragen.

 

Annotation Processor

Wir möchten nun einen sogenannten Annotationsprozessor schreiben, also ein Programm, welches unsere eigenen Annotationen ausliest und ausgibt, die wir an den analog benannten Methoden, an der Klasse und in einem Fall an den Methodenparametern angebracht haben. Enthalten die Annotationen Werte, so werden diese ebenfalls auf der Konsole ausgegeben. Die Hauptklasse mit ihrer main-Methode sieht wie folgt aus:

Listing 3

 

Methoden ohne Annotation

Listing 4

Zur Vereinfachung haben wir bislang meistens nur Methoden annotiert. Im ersten Beispiel geht es darum, alle Methoden auszugeben, die über keine Annotation verfügen. Start einer solchen Suche ist immer das Klassenobjekt, welches man durch .class oder getClass erhält. Hat man ein solches Klassenobjekt, in (Listing 4) als clazz bezeichnet, erhält man via getDeclaredMethods ein Array von Methods. Im Gegensatz zu getMethods gibt getDeclaredMethods zum einen auch die privaten Methoden einer Klasse zurück, zum anderen sind alle Methoden der Oberklasse(n) (inklusive Object) ausdrücklich nicht Bestandteil dieser Auflistung. Analog erhält man mit Method#getAnnotations ein Array aller Annotationen einer Methode. Ist dieses Array leer (also length == 0), dann folgt daraus, dass diese Methode nicht annotiert wurde. Sie ist damit genau das, was im hier vorliegenden Fall gesucht wird und ihr Methodenname (Method#getName) wird auf der Konsole ausgegeben.

 

Methoden mit Annotation ohne Wert

Listing 5

Wird nach einer ganz bestimmten Annotation gesucht, so bietet sich Method#getAnnotation(Class<T extends Annotation>) an. Als Parameter wird der gesuchte Annotationstyp mitgegeben. Wurde die entsprechende Annotation angebracht, so gibt der Aufruf ebendiese Annotation zurück; wenn nicht, dann ist der Rückgabewert null. Eine anschliessende Überprüfung auf null ist also immer obligatorisch, wenn man keine Laufzeitfehler riskieren will. In (Listing 5) wird gezielt nach der @NoValueAnnotation gesucht. Wird sie gefunden, dann wird der Name der Methode, an der sie angebracht wurde, auf der Konsole ausgegeben.

 

Methoden mit Annotation mit nur einem Wert

Listing 6

Das Vorgehen zum Detektieren der gesuchten Annotation (hier @SingleValueAnnotation) ist aus dem vorherigen Code-Beispiel bekannt und wird sich von nun an auch nicht mehr ändern. Neu ist in (Listing 6), wie sich der Wert value der Annotation abfragen lässt. Da bereits der konkrete Annotationstyp vorliegt, geschieht dies ganz einfach mit dem Aufruf von .value(). Im Beispiel wird der so ausgelesene Integer-Wert zusammen mit dem Methodennamen auf der Konsole ausgegeben.

 

Methoden mit Annotation mit mehreren Werten

Listing 7

Bei Annotationen, die mehrere Werte gleichen Typs aufnehmen können, liefert die Methode .value() ein Array statt eines einzelnen Wertes zurück. Der anschliessende Zugriff auf die Werte des Arrays ist trivial.

 

Methoden mit wiederholter Annotation mit Wert

Listing 8

Zwischen Annotationen mit mehreren Werten und @Repeatable Annotationen mit jeweils einem Wert, gibt es nicht nur beim Anbringen, sondern auch beim Auslesen leichte Unterschiede. Da nun mehrere Annotationen eines gesuchten gleichen Typs vorkommen können, funktioniert Method#getAnnotation nicht. Stattdessen muss Method#getAnnotationsByType(Class<T extends Annotation>) aufgerufen werden, welches nun ein Array von Annotationen des gesuchten Typs zurückgibt, sofern vorhanden. Im schlimmsten Fall ist dieses Array einfach leer; eine Überprüfung auf null kann entfallen. Um die Werte auszulesen, kann einfach dieses Array iteriert werden. Die darin enthaltenen Annotationen verhalten sich gleich wie die Annotationen mit nur einem Wert.

 

Methoden mit Annotation mit nur einem benannten Wert

Listing 9

Wir haben zuvor bereits auf den Parameter value zugegriffen. Sind die Werte einer Annotation anders benannt, so erfolgt ihr Zugriff analog anhand dieses Namens. Vorangehend wurde der Wert einer Annotation name genannt. Der Zugriff darauf erfolgt also einfach über .name().

Spätestens an dieser Stelle wird nun klar, wieso Annotationen ähnlich wie Interfaces implementiert werden und wieso die „Attribute“, sprich die Werte oder Parameter einer solchen Annotation immer als Anhang von Zugriffsmethoden, also mit einem nachfolgenden Klammerpaar deklariert werden müssen. Zur Laufzeit stellt diese Annotation nach aussen hin nichts anderes als ein Interface mit ebendiesen Zugriffsmethoden dar.

 

Methoden mit Annotation mit mehreren benannten Werten

Listing 10

Egal ob es sich um eine Annotation mit einem oder mehreren benannten Werten handelt, der Zugriff darauf erfolgt einheitlich und selbstklärend über den Namen des Wertes als Methodenaufruf, in (Listing 10) mit .first(), .second() und .third().

 

Methoden mit Annotation mit Wert als Aufzählungstyp

Listing 11

Auch der Zugriff auf Werte einer Annotation, die einen Aufzählungstyp darstellen, ist nicht besonders schwer. Wenn der Aufzählungstyp wie im vorliegenden Code-Beispiel direkt in der Annotation definiert ist, so erhält man auch nur über diese Annotation Zugriff auf diesen Typ. In (Listing 11) ist dies gut ersichtlich an EnumValueAnnotation.GreekLetter.

 

Klassen mit @Inherited-Annotation

Listing 12

Wechseln wir von annotierten Methoden zu annotierten Klassen. Die beiden Codebeispiele in (Listing 12), die sich nur am Annotationstyp unterscheiden, sollen demonstrieren, welche Auswirkung die @Inherited Meta-Annotation auf die Vererbung von Annotationen hat. Die Ausgabemethoden werden jeweils sowohl mit der Klasse Super als auch mit der Unterklasse Sub aufgerufen. Wie zu erwarten, ist die InheritedAnnotation sowohl in Super als auch in Sub zugreifbar, die NonInheritedAnnotation hingegen nur in Super.

 

Methodenparameter mit Annotation

Listing 13

Schauen wir uns zum Abschluss noch an, wie annotierte Methodenparameter ausgelesen werden können. Mit Method#getParameters lässt man sich via Reflection ein Parameter Array geben, welches anschließend iteriert wird. Selbsterklärend erhält man via Parameter#getAnnotation(Class<T extends Annotation>) Zugriff auf eine gesuchte Parameter-Annotation, sofern vorhanden. Das Auslesen ihrer Werte erfolgt auf die bekannte Art.

 

Fazit

Die Literatur zum Thema Annotationen ist recht begrenzt und die Syntax sehr gewöhnungsbedürftig. Dennoch zeigt sich auf beeindruckende Weise, wie mächtig und flexibel Annotationen in Java eingesetzt werden können. Unserer 3-teiligen Serie soll dazu beitragen, anfängliche Berührungsängste gegenüber Annotationen zu beseitigen und ihren sinnvollen Einsatz in Softwareprojekten fördern.

 

Christian Heitzmann ist Gründer und Geschäftsführer der SimplexaCode AG in Luzern, die sich auf Software-Entwicklung, -Schulung und -Beratung v.a. für MINT-Anwendungen und technische Implementierungsthemen in Java spezialisiert hat. Er ist seit 15 Jahren mit Java vertraut und hat während vieler Jahre Algorithmen und Mathematik unterrichtet.

Startseite

Redaktion


Leave a Reply