Java Annotationen

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

Registriere dich um viele Vorteile zu genießen! Weniger Werbung, bessere Kommunikation und vieles mehr!

  • Aufbauend auf der Anleitung zu Interfaces in Java lernst du heute die Annotationen kennen.
    Benutzt man viel fremde Bibliotheken, so läuft man ihnen häufiger über den Weg.
    Wichtig ist, dass du dir über den Zweck und die Funktionsweise von Interfaces klar bist.
    Java Annotationen
    In dieser Anleitung lernst du die Annotationen und ihren Verwendungszweck sowie einiges über ihre Funktionsweise kennen.
    Du wirst durch mehrere Praxisbeispiele ein Gefühl für den Einsatz der Annotationen im API-Design bekommen.
    Vorweg sei gesagt: Fortgeschrittene Java-Kenntnisse sollten gegeben sein. Annotationen sind in Java ein Konzept der Objektorientierung und erfordern zudem Kenntnisse in Sachen Metaprogramming. Ich werde versuchen, die Reflection-API möglichst wenig einzusetzen. Dennoch solltest du hier über Grundwissen verfügen.

    Am Ende dieser Anleitung schreiben wir uns eine eigene Listener-Architektur, wie sie in Bukkit integriert ist. Dazu werden Annotationen und Interfaces mit Metaprogramming kombiniert.

    Annotationen in der Praxis

    Das folgende Listing sollte einem Bukkit-/Spigot-Entwickler bekannt vorkommen.

    Java-Quellcode

    1. @EventHandler
    2. public void onPlayerJoin (PlayerJoinEvent event) {
    3. // do stuff
    4. }
    Listing 1 - @EventHandler ist eine Annotation

    Die Erklärung dafür ist nicht ganz trivial, aber nachvollziehbar:
    1. Wir schreiben eine Klasse, die das Interface Listener implementiert
    2. In dieser Klasse legen wir beliebig viele Methoden an
    3. Bestimmte Methoden versehen wir mit @EventHandler
    Und der letzte Punkt ist entscheidend: Wie soll Bukkit wissen, welche Methoden aufgerufen werden müssen, wenn ein Spieler den Server betritt? Die Annotation markiert: Offenbar findet Bukkit heraus, welche Methoden mit @EventHandler versehen wurden. Du kannst es überprüfen: Schreibe eine Methode, die nicht mit @EventHandler versehen wurde und überprüfe, ob diese aufgerufen wird.

    Jetzt hast du einen Eindruck davon, wie Annotationen aussehen, wenn man solche benutzt. Bevor wir eine Annotation benutzen können, müssen wir eine erstellen.



    Eigene Annotationen
    Es ist an der Zeit, eine eigene Annotation (oder auch: einen Annotationstypen) anzulegen. Ich werde jetzt eine sehr einfache parameterlose Annotation anlegen, die auf alle Ziele (engl. Targets) angewandt werden kann. Wir werden bezüglich der Targets noch Einschränkungen festlegen und auch Parameter hinzufügen. Doch zunächst ein kleines Beispiel.

    Java-Quellcode

    1. public @interface MyAnnotation {
    2. }
    Listing 2 - Die erste Annotation

    Wie sich erkennen lässt, wurde ein bekanntes Schlüsselwort abgeändert: Wir benötigen für Annotationen @interface.
    Diese Abwandlung selbst ist noch keine Annotation - auch wenn es danach aussieht.
    Mit dem Code aus Listing 2 haben wir einen Annotationstypen deklariert. Diesen können wir nun auf mehrere Ziele anwenden: Unter anderem Klassen, Methoden oder Membervariablen). Eine vollständige Liste findet sich im Anhang.



    Retention
    Eine ganz wichtige Eigenschaft, die wir auf noch verändern müssen, ist die Retention (engl. für Beibehaltung).
    Durch diese Eigenschaft wird festgelegt, zu welchem Zeitpunkt uns diese Annotation zur Verfügung steht. Es gibt hier drei Optionen:
    • SOURCE (Nur auf Quelltextebene; durch den Compiler entfernt)
    • CLASS (Im Bytecode; muss zur Laufzeit manuell geladen werden)
    • RUNTIME (Zur Laufzeit)
    Die Optionen werden nach unten erweitert, d.h. RUNTIME steht ebenfalls auf Quelltextebene und im Bytecode zur Verfügung. Ein anderes Verhalten wäre auch technisch nicht möglich, da vom Compiler entfernte Eigenschaften zur Laufzeit gar nicht mehr geladen werden können.

    Wie setzen wir nun diese Eigenschaft?
    Dies erfolgt über eine Annotation. Im folgenden Listing wird ein einfaches Beispiel dazu gegeben.

    Java-Quellcode

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @interface MyAnnotation {
    3. }
    Listing 3 - Retention festlegen

    Es sollte nicht unerwähnt bleiben, dass der Standardwert hier CLASS ist. Falls du mithilfe der Reflection-API auf diese Annotation zugreifen bzw. damit arbeiten möchtest, so solltest du immer RUNTIME verwenden. Für Operationen auf Quellcodeebene genügt auch SOURCE, @Override ist ein Beispiel dafür. Diese Annotation spielt eine besondere Rolle: Ist eine Methode mit ihr versehen, so wird das Java-Programm nur einwandfrei kompiliert, wenn in einer Superclass eine Methode mit gleicher Signatur existiert, sie also überschrieben wird. Andernfalls bricht der Compiler den Vorgang ab und quittiert dies mit einer Fehlermeldung.



    Targets
    Auch ist es möglich, die Menge der annotierbaren Element festzulegen. So ist es uns möglich, eine Annotation zu deklarieren, die nur auf Klassen oder nur auf Membervariablen angewandt werden kann. Ich habe soeben von einer Menge gesprochen - das ist auch korrekt, denn eine Annotation kann auch auf mehrere Elemente angewandt werden. Dies wird im folgenden Listing deutlich:

    Java-Quellcode

    1. // Hinweis: ElementType.TYPE ist stellvertretend für Klassen verwendet
    2. @Target(value = { ElementType.TYPE, ElementType.METHOD })
    3. public @interface MyAnnotation {
    4. }
    Listing 4 - Mit @Target die Menge der Zielelemente definieren

    Um mehrere Typen anzugeben, muss einfach eine komma-separierte Liste, eingeschlossen in zwei geschweiften Klammern, als Wert angegeben werden.

    Hinweis: In diesem Fall könnte man value = ... sogar weglassen, da es nur einen Parameter gibt. Wie Parameter behandelt werden, schauen wir uns noch an.



    Annotationen für Annotationen

    Ist es dir aufgefallen?
    @Target und @Retention sind selbst Annotationen. Daraus können wir schließen, dass Annotationen sich auf Annotationen anwenden lassen. Und das stimmt: Es gibt sogar einen ElementType dafür: ANNOTATION_TYPE. Fügen wir diesen in die Menge bei @Target ein, so lässt sich unsere Annotation über Annotationen schreiben.



    Parameter
    Sogar Parameter wurden hier schon verwendet: Sowohl bei @Target, als auch bei @Retention. Viele weitere Annotationen (z.B. @EventHandler) gibt es auch Parameter.
    Um etwas konkreter zu werden: Es existieren zwei Arten von Parameter. Einmal Parameter mit Standardwert und solche ohne Standardwert. Der Standardwert spielt für den Benutzer unserer Annotation eine signifikante Rolle: Ist kein Standardwert gesetzt, so ist der Benutzer gezwungen, den Parameter explizit mit einer Wertzuweisung anzugeben. Wir werden uns beide Arten anschauen.
    Doch vorher noch eine Information: Parameter zwischen der öffnenden geschweiften Klammer { und der schließenden geschweiften Klammer } nach der Deklaration via public @interface <NAME> deklariert.
    Im folgenden Listing wird eine Annotation mit einem Parameter ohne Standardwert erstellt.

    Java-Quellcode

    1. // Platz für Eigenschaften
    2. public @interface MyAnnotation {
    3. String value();
    4. }
    Listing 5 - Ein einfacher String-Parameter

    Es sieht ganz ähnlich aus wie Methoden in normalen Interfaces, mit dem Unterschied, dass hier nur bestimmte Werte möglich sind. Wir dürfen nämlich nur primitive Datentypen, Strings, Enum-Typen, Class-Objekte, Annotations-Typen (etwas spezieller; hier nicht behandelt) und einfache Arrays von den genannten Typen als Werte festlegen.

    Hinweis: Verwendet man nur einen Parameter, so wird dieser häufig einfach value genannt.

    Versehen wir ein Element mit unserer Annotation, so werden Parameter in zwei runden Klammern danach in einer komma-separierten, assoziativen Liste angegeben. Dazu ein kleines Beispiel: @MyAnnotation(value="Wert1", param2=40, ...). Das sieht nicht nur einfach aus, sondern soll es auch sein.

    Bevor wir diesen Abschnitt abschließen, noch ein Listing zu optionalen Parameter, also denen ohne Standardwert.

    Java-Quellcode

    1. // ...
    2. public @interface MyAnnotation {
    3. int value() default 42;
    4. }
    Listing 6 - Optionale Parameter

    Wichtig ist, dass unser Semikolon nach wie vor am Ende der Zeile steht. Mit dem Schlüsselwort default leiten wir einen Standardwert ein, welcher dann, angegeben als Literal oder Konstante, folgt. Und weil es so wichtig ist, muss es noch einmal gesagt sein: Wir dürfen hier keine Variablen angeben. Eine statische, aber nicht mit final deklariert Variable, würde nicht ausreichen. Es muss sich um eine Konstante oder besser noch ein Literal handeln. Es ist auch das Ergebnis einer arithmetischen Operation mit Konstanten und Literalen möglich. Wichtig ist, dass das Ergebnis zur Compiletime bekannt ist. In Listing 6 habe wurde ein Literal verwendet, nun noch einmal das gleiche Beispiel ein wenig verändert (mit Konstante):

    Java-Quellcode

    1. // ...
    2. public @interface MyAnnotation {
    3. float value() default (float) Math.PI;
    4. }
    Listing 7 - Eine Konstante mit expliziter Typumwandlung als Standardwert

    Ich habe hier bewusst float als Typ gewählt. Dadurch, dass Math.PI vom Typ double ist, ist eine Typumwandlung vonnöten. An dieser Stelle bedienen wir uns der expliziten Typumwandlung via Cast. Einfach zu Demonstrationszwecken.



    Sichtbarkeiten
    Weil es hier öfter zu Missverständnissen kommt, möchte ich noch kurz die möglichen Sichtbarkeitslevel für Annotationen auflisten, denn es gibt nur zwei:
    • Public - Die Annotation ist von überall aus erreichbar
    • Package - Die Annotation kann nur innerhalb eines Packages benutzt werden
    Um letzteren Sichtbarkeitsgrad zu erreichen, muss einfach das public bei der Deklaration einer Annotation entfernt werden. Dies wird jedoch ein seltener Fall sein, da Annotationen meistens im API-Design gebraucht werden.



    Praxisbeispiel: Eine Listener-Architektur
    Nun hast du ein mächtiges Werkzeug in der Hand, um noch schöneren Programmcode zu fabrizieren und APIs einfacher zu gestalten. Zeit, dieses Wissen anzuwenden. Genau darum geht es in diesem Zusatzkapitel: Wir wollen eine eigene Listenerarchitektur entwickeln. Ich werde dieses Kapitel in drei Abschnitte unterteilen:
    1. Konzept
    2. Praktische Umsetzung
    3. Anwendungsbeispiel
    Am Ende folgt ein Anwendungsbeispiel - wir schauen uns an, wie ein anderer Entwickler unsere Architektur verwenden kann.
    Doch Eines nach dem Anderen.

    1 - Konzept
    Wir müssen jetzt planen, wie die Architektur aufgebaut sein wird.
    Dazu sollte dir Observer Pattern ein Begriff sein. Dabei handelt es sich um ein Entwurfsmuster, das wir hier auch verwenden werden. Falls du dieses nicht kennst, ist das nicht weiter problematisch, du könntest höchstens etwas überrascht auf einzelne Codesegmente reagieren.

    Folgendes soll beim API-User erzielt werden: Man erstellt eine beliebige Klasse und implementiert ein (Marker-)Interface von uns. Nun lassen sich beliebig viele Methoden deklarieren und definieren, die optional mit unserer Annotation versehen werden können. Nach dem Erstellen der Klasse erzeugt man eine Instanz und registriert diese über eine statische Methode von uns. Wird nun eine weitere statische Methode von uns aufgerufen, so werden alle registrierten Objekte iteriert und jeweils alle mit unserer Annotation versehenen Methoden mit einem bestimmten Parameter aufgerufen. Dieser Parameter ist ein Event. Die Methoden werden nur dann aufgerufen, wenn der Typ des Parameters mit der Klasse des Events kompatibel ist. Erstellt der API-User eine parameterlose annotierte Methode oder verwendet als ersten Parameter einen Typ, der nicht mit Event kompatibel ist, so wird die Methode gar nicht erst aufgerufen.

    Das klingt zunächst etwas trocken und auch etwas kompliziert. Falls du nicht auf Anhieb verstanden hast, was gemeint ist, lies den Text bitte noch einmal in Ruhe für dich durch. Es ist wichtig, zu verstehen, wie der Plan aussieht. Auf ein UML-Klassendiagramm möchte ich an dieser Stelle verzichten.


    2 - Praktische Umsetzung
    Nun geht's ans Werk.
    Ich habe den vollständigen Quellcode vorbereitet und in einen Spoiler verpackt. Bevor du dir die Klassen, Interfaces und Annotationen anschaust, wäre es vorteilhaft, selbst einen Versuch zu starten und den obigen Entwurf umzusetzen. Alternativ kannst du auch direkt meinen Quellcode anschauen und vielleicht noch etwas verbessern / erweitern. Der Quellcode wird durch deutsche Kommentare etwas erklärt.

    Quellcode
    Spoiler anzeigen

    Java-Quellcode: Listener.java

    1. public interface Listener {
    2. // Dient nur als Marker-Interface
    3. }

    Java-Quellcode: Event.java

    1. public interface Event {
    2. void printName();
    3. }

    Java-Quellcode: EventHandler.java

    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. @Retention(RetentionPolicy.RUNTIME)
    6. @Target(ElementType.METHOD)
    7. public @interface EventHandler {
    8. // no parameters
    9. }

    Java-Quellcode: EventManager.java

    1. import java.lang.reflect.InvocationTargetException;
    2. import java.lang.reflect.Method;
    3. import java.util.ArrayList;
    4. import java.util.Iterator;
    5. import java.util.List;
    6. public class EventManager {
    7. private static List<Listener> listeners;
    8. static {
    9. listeners = new ArrayList<>();
    10. }
    11. public static void register(Listener l) {
    12. listeners.add(l);
    13. }
    14. public static void unregister(Listener l) {
    15. listeners.remove(l);
    16. }
    17. public static void call(Event event) {
    18. event.printName();
    19. Iterator<Listener> it = listeners.iterator();
    20. // Alle Listener iterieren
    21. while (it.hasNext()) {
    22. Listener l = it.next();
    23. // Alle Methoden iterieren
    24. for (Method m : l.getClass().getMethods()) {
    25. // Prüfen, ob Annotation gesetzt
    26. if (m.isAnnotationPresent(EventHandler.class)) {
    27. // Prüfen, ob der erste Parameter mit event (siehe Parameter) kompatibel ist
    28. if (m.getParameterTypes().length == 1 && m.getParameterTypes()[0].isAssignableFrom(event.getClass())) {
    29. try {
    30. m.invoke(l, event);
    31. } catch (IllegalAccessException e) {
    32. e.printStackTrace();
    33. } catch (IllegalArgumentException e) {
    34. e.printStackTrace();
    35. } catch (InvocationTargetException e) {
    36. e.printStackTrace();
    37. }
    38. }
    39. }
    40. }
    41. }
    42. }
    43. }
    Alles anzeigen



    3 - Anwendungsbeispiel
    Was nützt uns eine noch so schöne Architektur, wenn wir sie nicht benutzen können?
    Ich gebe einfach mal ein Beispiel und werde es danach ein wenig erklären.

    Java-Quellcode

    1. public class TestMain {
    2. public static void main(String[] args) {
    3. new TestMain();
    4. }
    5. // Called from main method
    6. public TestMain() {
    7. EventManager.register(new TestListener());
    8. EventManager.call(new TestEvent());
    9. }
    10. private class TestEvent implements Event {
    11. @Override
    12. public void printName() {
    13. System.out.println("Das Test-Event wurde gecalled!");
    14. }
    15. }
    16. private class Event2 implements Event {
    17. @Override
    18. public void printName() {
    19. System.out.println("Das 2. Event wurde gecalled!");
    20. }
    21. }
    22. private class TestListener implements Listener {
    23. @EventHandler
    24. public void onTest(TestEvent event) {
    25. System.out.println("Der Listener hat auf das TestEvent reagiert!");
    26. }
    27. @EventHandler
    28. public void onEvent2(Event2 event) {
    29. System.out.println("Der Listener hat auf das 2. Event reagiert!");
    30. }
    31. @EventHandler
    32. public void onEvent(Event event) {
    33. System.out.println("Der Listener hat auf irgendein Event reagiert!");
    34. }
    35. }
    36. }
    Alles anzeigen
    Listing 8 - Ein Anwendungsbeispiel

    Die Ausgabe dieses Beispiels ist korrekterweise folgende:

    Quellcode

    1. Das Test-Event wurde gecalled!
    2. Der Listener hat auf das TestEvent reagiert!
    3. Der Listener hat auf irgendein Event reagiert!
    Zunächst wird eine Instanz unserer Listener-Klasse beim EventManager über EventManager#register registriert. Diese Instanz wurde einer ArrayList hinzugefügt.
    Der Listener besitzt drei Methoden:
    • onTest(TestEvent event) - reagiert nur, wenn ein Event vom Typ TestEvent aufgerufen wurde
    • onEvent2(Event2 event) - reagiert nur, wenn ein Event vom Typ Event2 aufgerufen wurde
    • onEvent(Event event) - reagiert nur, wenn ein Event vom Typ Event aufgerufen wurde (also jedes beliebige Event)
    Es wäre nun auch möglich, eine Klasse zu schreiben, die von TestEvent abgeleitet ist. Unsere erste Methode würde ebenfalls darauf reagieren, da sie ja nur eine Instanz von TestEvent als Parameter fordert. Dieses Verhalten ist für den API-User sehr komfortabel.

    Nun wird unser Event aufgerufen, über EventManager#call. Diese Methode wiederum iteriert alle Listener, geht hier wiederum alle Methoden durch, prüft die Annotation und die Parameter und rufst die Methode schlussendlich im Erfolgsfall auf.



    Schlusswort
    Ich hoffe, dir hat das Lernen mit dieser Anleitung Spaß gemacht.
    Vielleicht findest du schon bald eine Möglichkeit, Annotationen in dein API-Design einfließen zu lassen und den Code noch effizienter zu gestalten.
    Mit der Zeit wirst du ein Gefühl dafür bekommen, wann Interfaces und Annotationen sich gut einsetzen lassen. Versuche, diese dann einzusetzen, wenn der Code dadurch lesbarer und verständlicher wird.

    Schreibe nachhaltigen Code!

    Janhektor


    Anhang A - Ziele (ElementTypes)
    Im Folgenden findest du eine Auflistung aller Targets für Annotationen (ElementType-Enum) mit dem entsprechenden Ziel.
    • ANNOTATION_TYPE - Annotationen
    • CONSTRUCTOR - Konstruktoren
    • FIELD - Klassenattribute / Membervariablen
    • LOCAL_VARIABLE - Lokale Variablen
    • METHOD - Methoden
    • PACKAGE - Packages, eingetragen in der optionalen package-info.java Datei
    • PARAMETER - Parameter bei Methoden und Konstruktoren
    • TYPE - Klassen
    ------------------------------------------------------------

    538 mal gelesen