W3docs

Benutzerdefinierte Java-Annotationen

Eigene Annotationstypen in Java deklarieren, Retention und Targets konfigurieren und zur Laufzeit per Reflection auslesen.

Eine benutzerdefinierte Annotation ist ein Annotationstyp, den Sie selbst deklarieren – im Gegensatz zu einem vom JDK (wie @Override) oder einem Framework (wie @Test) bereitgestellten. Die Syntax ähnelt einem Interface, die Regeln sind jedoch strenger. Nach der Deklaration wird Ihre Annotation zu einem echten Typ, den Sie an Code anheften, per Reflection nachschlagen und zur Compile-Zeit verarbeiten können.

Dieses Kapitel ist der praktische Leitfaden zum Schreiben eigener Annotationen: das Schlüsselwort @interface, welche Elementtypen erlaubt sind, wie sich erforderliche und optionale Elemente unterscheiden und wie ein Prozessor die Werte zur Laufzeit zurückliest. Wenn Sie Annotationen noch nicht kennen, beginnen Sie mit Java-Annotationen und den eingebauten Annotationen; um zu steuern, wo Ihre Annotation erscheinen darf und wie lange sie lebt, lesen Sie Meta-Annotationen.

Die @interface-Deklaration

Ein Annotationstyp wird mit dem Schlüsselwort @interface deklariert:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audited {
  String value();                                       // required element
  String level() default "INFO";                        // element with a default
  String[] tags() default {};                           // array element with default
}

Damit wird ein neuer Annotationstyp namens Audited deklariert, dessen Elemente wie Methoden auf einem Interface aussehen, sich jedoch als benannte Werte an Verwendungsstellen verhalten. Jede „Methode" ist ein Element.

Verwendung:

@Audited("UserService.login")                            // value omitted name → "value" element
public User login(String user, String password) { ... }

@Audited(value = "Service.save", level = "WARN", tags = {"db", "write"})
public void save(Entity e) { ... }

Die value-Kurzschreibweise (@Audited("...") statt @Audited(value = "...")) ist nur verfügbar, wenn das Element buchstäblich value heißt. Deshalb verwenden so viele Annotationen genau diesen Namen für ihren primären Parameter.

Erlaubte Elementtypen

Der Rumpf eines @interface besteht aus einer geschlossenen Menge von Elementdeklarationen. Der Rückgabetyp jedes Elements muss eines der Folgenden sein:

  • Ein primitiver Typ (int, long, double, boolean, ...).
  • String.
  • Class oder ein parametrisiertes Class<?>.
  • Ein Enum-Typ.
  • Ein anderer Annotationstyp.
  • Ein Array aus einem der obigen Typen.

Standardwerte werden mit default angegeben. Der Standardwert muss eine Compile-Zeit-Konstante des richtigen Typs sein:

@interface RetryPolicy {
  int attempts() default 3;
  long delayMs() default 100;
  Class<? extends Exception>[] on() default {Exception.class};
  Level level() default Level.WARN;
  enum Level { DEBUG, INFO, WARN, ERROR }
}

Was Sie nicht in einer Annotation deklarieren können:

  • Methoden mit Parametern (die () sind erforderlich, aber immer leer).
  • Generische Elemente (<T> T value(); ist unzulässig).
  • throws-Klauseln.
  • Vererbung von einem anderen Interface (Annotationen erweitern implizit java.lang.annotation.Annotation).
  • Konstruktoren.

Sie können Typen innerhalb einer Annotationsdeklaration verschachteln – das obige Level-Enum lebt innerhalb von @RetryPolicy. Das ist ein nützliches Idiom: Es hält zusammengehörige Optionen auf die Annotation beschränkt, die sie verwendet.

Erforderliche vs. optionale Elemente

Ein Element ohne default ist an Verwendungsstellen erforderlich. Der Compiler schlägt fehl, wenn Sie es vergessen:

@interface Issue { String id(); }                       // required

@Issue                                                  // compile error: missing 'id'
public void brokenLogin() { }

@Issue(id = "JIRA-123")                                 // OK: 'id' supplied
public void fixedLogin() { }

Ein stilistischer Hinweis: Wenn es einen einzigen offensichtlichen Wert gibt, benennen Sie das Element value und machen Sie es erforderlich. Wenn es mehrere Parameter gibt, benennen Sie sie und geben Sie sinnvolle Standardwerte an, damit der häufige Aufruf kurz bleibt.

Marker-Annotationen

Eine Annotation ohne Elemente ist ein Marker:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface ThreadSafe { }

Marker-Annotationen tragen keine Daten; ihre An- oder Abwesenheit ist das gesamte Signal. Per Reflection fragt man: „Hat diese Klasse @ThreadSafe?" mit getAnnotation(ThreadSafe.class) != null oder isAnnotationPresent(ThreadSafe.class).

Annotationen zur Laufzeit auslesen

Bei einer RUNTIME-Annotation stellt Reflection mehrere Methoden auf Class, Method, Field, Constructor und Parameter bereit (die vollständige Oberfläche finden Sie unter Annotationen mit Reflection auslesen):

  • isAnnotationPresent(Class) — schnelles Ja/Nein.
  • getAnnotation(Class) — gibt die Annotationsinstanz zurück oder null.
  • getAnnotations() — gibt alle Annotationen des Elements zurück (deklarierte + über @Inherited geerbte).
  • getDeclaredAnnotations() — nur die direkt am Element deklarierten, ohne @Inherited.
  • getAnnotationsByType(Class) — behandelt den @Repeatable-Fall korrekt.

Das Auslesen hat unabhängig vom verwendeten Zieltyp die gleiche Form:

Method m = ...;
if (m.isAnnotationPresent(Audited.class)) {
  Audited a = m.getAnnotation(Audited.class);
  log(a.value(), a.level(), a.tags());
}

Das zurückgegebene Audited ist ein JVM-generierter Proxy – die Elementmethoden (value(), level(), tags()) sind echte Methodenaufrufe darauf.

Annotationsgleichheit, Identität und toString

Annotationswerte implementieren equals, hashCode und toString gemäß der Definition in java.lang.annotation.Annotation:

  • Zwei Annotationsinstanzen sind gleich, wenn sie denselben Typ haben und jedes Element gleich ist (mit tiefer Array-Gleichheit).
  • hashCode wird auf definierte Weise aus den Elementwerten abgeleitet.
  • toString erzeugt eine stabile, quelltextähnliche Darstellung – nützlich für Logging.

Reflection gibt manchmal denselben Proxy bei wiederholten Nachschlägen am selben Element zurück, manchmal einen neuen. Verwenden Sie equals, niemals ==, beim Vergleich von Annotationsinstanzen.

Ein vollständiges Beispiel: deklarieren, anheften und reflektieren

Das Programm deklariert zwei Annotationen (@Audited und @Retry), verwendet sie in einer Klasse und durchläuft die Methoden per Reflection – jede Methode wird entweder in einem Auditing-Wrapper oder mit einer Retry-Schleife ausgeführt. Die Annotationen sind reine Metadaten; das Verhalten steckt im Executor.

java— editable, runs on the server

Was aus dem Programmablauf festzuhalten ist:

  • greet trug nur @Audited, daher druckte der Executor ein Enter/Exit-Paar rund um die Methode, ohne einen Retry. Denselben Executor wendete save mit @Retry zusätzlich zu @Audited an: Der erste Aufruf warf eine Ausnahme (saveCalls == 1), der Helfer protokollierte den Fehler und wiederholte, und der zweite Versuch gab saved: data zurück. Die Annotationen selbst taten nichts – der invoke-Helfer lieferte das Verhalten.
  • unannotated durchlief dieselbe Schleife, weil der Executor einheitlich ist. isAnnotationPresent gab für beide Annotationen false zurück, daher hat der Helfer weder geloggt noch wiederholt; die Methode wurde einfach einmal ausgeführt. Das ist das Muster für Prozessoren: Annotationen prüfen, sinnvoll reagieren, wenn sie fehlen, und nie einen Sonderfall für „das ist der annotierte Pfad" einbauen.
  • Jeder Elementzugreifer (a.value(), r.attempts(), r.when()) gab den im Quelltext geschriebenen Wert zurück. Retry.when() kam als Enum-Konstante ALWAYS zurück, weil die Aufrufstelle den Standardwert verwendete. Standardwerte werden vom Compiler in den Annotations-Proxy eingebacken; der Aufrufer kann nicht unterscheiden, ob ein Wert explizit angegeben oder defaulted wurde.
  • Das toString von Audited druckte eine quelltextähnliche Form wie @...Audited(level="WARN", value="Service.save"). Das ist eine Eigenschaft jedes Annotation-Proxys – nützlich für Logging und assertEquals in Tests. (Die Reihenfolge der Elemente in Klammern ist nicht garantiert und variiert zwischen JDK-Versionen, daher sollte man nicht auf den genauen String assertieren.)
  • Die beiden Annotationen sind auf Quelltextebene völlig unabhängig: Eine Methode trägt beide gleichzeitig, und Reflection gab beide problemlos zurück. Es gibt keine Vererbungshierarchie zwischen Annotationstypen; das Kombinieren von Verhalten wird durch das Stapeln von Annotationen auf demselben Element erreicht, nicht durch das Erweitern einer Annotation von einer anderen.

Wo es nicht funktioniert

Einige häufige Überraschungen:

  • Source-Retention kann nicht reflektiert werden. Wenn Sie @Retention(RUNTIME) vergessen, gibt Reflection stillschweigend null zurück. Der Standardwert ist CLASS, nicht RUNTIME.
  • Targets müssen übereinstimmen. Wenn @Target(METHOD) gesetzt ist und Sie die Annotation auf eine Klasse setzen, verweigert der Compiler die Arbeit.
  • Elementstandards müssen Compile-Zeit-Konstanten sein. Sie können nicht auf new ArrayList<>() defaulten; Sie können auf {} für ein Array, eine Enum-Konstante, ein Class-Literal oder ein primitives Literal defaulten.
  • Annotationen können sich nicht zyklisch referenzieren. Ein Element vom Typ MyAnn innerhalb von @interface MyAnn wird abgelehnt.

Das nächste Kapitel, Annotation Processing, zeigt die Compile-Zeit-Seite – das Erzeugen neuer Quelldateien als Reaktion auf Ihre benutzerdefinierten Annotationen, anstatt (oder zusätzlich zu) sie zur Laufzeit auszulesen.

Übungen

Übung
Sie deklarieren `@Cached { int ttlSeconds(); }` und setzen es auf eine Methode. Zur Laufzeit gibt `m.getAnnotation(Cached.class)` `null` zurück, obwohl der Quelltext eindeutig `@Cached(ttlSeconds = 60)` enthält. Was ist die wahrscheinlichste Ursache?
Sie deklarieren `@Cached { int ttlSeconds(); }` und setzen es auf eine Methode. Zur Laufzeit gibt `m.getAnnotation(Cached.class)` `null` zurück, obwohl der Quelltext eindeutig `@Cached(ttlSeconds = 60)` enthält. Was ist die wahrscheinlichste Ursache?
Was this page helpful?