W3docs

Java Reflection: Annotationen lesen

Annotationsmetadaten zur Laufzeit in Java mit Reflection lesen — getAnnotation, getAnnotations.

Frühere Kapitel behandelten das Deklarieren von Annotationen (siehe Custom Annotations und Meta-Annotations); dieses Kapitel befasst sich damit, diese zur Laufzeit über Reflection auszulesen. Eine Annotation mit @Retention(RUNTIME) kann auf der Class, Method, Field, Constructor und dem Parameter, dem sie zugeordnet ist, abgefragt werden. Das Auslesen von Annotationen ist die Art, wie JUnit @Test findet, wie Spring @Autowired findet und wie Validierungs-Frameworks @NotNull finden. Dieses Kapitel fasst die vollständige Lese-API an einem Ort zusammen, einschließlich der Besonderheiten von @Inherited und @Repeatable.

Diese Seite behandelt die vier AnnotatedElement-Lesemethoden, warum Retention eine harte Voraussetzung ist, wie @Inherited und @Repeatable das Ergebnis beeinflussen, wie Parameter-Annotationen gelesen werden, und ein ausgearbeitetes Validierungs-Scanner-Beispiel, das ausgeführt werden kann.

Die vier Lesemethoden

Jedes annotierbare Element (Class, Method, Field, Constructor, Parameter) implementiert AnnotatedElement, das überall dieselben vier Methoden definiert:

AnnotatedElement el = SomeClass.class;   // or a Method, Field, etc.

el.isAnnotationPresent(Audited.class);   // boolean — quick check
el.getAnnotation(Audited.class);         // the annotation instance, or null
el.getAnnotations();                     // ALL annotations (declared + inherited)
el.getDeclaredAnnotations();             // only those declared directly here

Da die API einheitlich ist, ist der Code zum Auslesen einer Annotation von einer Methode identisch mit dem Auslesen von einer Klasse — man hält einfach ein anderes AnnotatedElement. Abgerufene Annotationswerte sind JVM-generierte Proxys; der Aufruf von a.value() ist ein echter Methodenaufruf, der den zur Kompilierzeit eingebauten Elementwert zurückgibt.

Retention ist eine Voraussetzung

Dies sei wiederholt, da es der häufigste Fehler ist: Nur Annotationen mit RUNTIME-Retention sind für Reflection sichtbar.

@Retention(RetentionPolicy.RUNTIME)   // <-- required for reflection
@interface Audited { String value(); }

Die Standard-Retention ist CLASS, welche die Annotation in der .class-Datei behält, sie aber vor der Laufzeit verwirft. SOURCE-Retention verwirft sie sogar noch früher. Wenn getAnnotation bei einer Annotation null zurückgibt, die im Quellcode klar zu sehen ist, liegt es fast immer an der fehlenden @Retention(RUNTIME).

getAnnotations vs. getDeclaredAnnotations und @Inherited

Der Unterschied zwischen diesen beiden ist @Inherited. Standardmäßig werden Annotationen nicht von Unterklassen geerbt. Aber wenn ein Annotationstyp selbst mit @Inherited meta-annotiert ist, dann erbt eine Unterklasse eine Annotation auf Klassenebene von ihrer Oberklasse:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Component { }

@Component class Base { }
class Derived extends Base { }     // Derived has no @Component in source

Derived.class.getAnnotation(Component.class)          // → present! (inherited)
Derived.class.getDeclaredAnnotation(Component.class)  // → null (not declared here)

getAnnotations() schließt also geerbte Annotationen ein; getDeclaredAnnotations() meldet nur das, was physisch auf diesem Element steht. Zwei wichtige Einschränkungen: @Inherited funktioniert nur für Klassen-Annotationen (nicht für Methoden oder Felder) und nur entlang der Oberklassen-Kette (nicht für Interfaces).

Wiederholbare Annotationen

Seit Java 8 kann eine mit @Repeatable markierte Annotation mehrfach auf einem Element erscheinen. Intern bündelt der Compiler Wiederholungen in eine Container-Annotation, sodass getAnnotation sie nicht sieht — man verwendet getAnnotationsByType, das den Container transparent entpackt:

@Repeatable(Roles.class)
@Retention(RetentionPolicy.RUNTIME)
@interface Role { String value(); }
@Retention(RetentionPolicy.RUNTIME)
@interface Roles { Role[] value(); }     // the container

@Role("admin") @Role("user") class Account { }

Account.class.getAnnotationsByType(Role.class);   // → [Role(admin), Role(user)]
Account.class.getAnnotation(Role.class);          // → null! (it's wrapped in Roles)

Verwende getAnnotationsByType(Role.class) für wiederholbare Annotationen; es gibt ein Array zurück und behandelt sowohl den Einzel- als auch den Wiederholungsfall.

Parameter- und andere Ziel-Annotationen lesen

Parameter erhalten ihre eigenen Annotationen über die zweidimensionale Method.getParameterAnnotations() (ein Array pro Parameter) oder die übersichtlichere Parameter-API:

for (Parameter p : method.getParameters()) {
  if (p.isAnnotationPresent(NotNull.class)) { /* validate */ }
}

Dieselben AnnotatedElement-Methoden funktionieren auf Field, Constructor, Package und sogar auf Annotationen selbst (um Meta-Annotationen wie @Retention auszulesen). Wie man die Handles für Method, Field und Constructor überhaupt erst erhält, ist unter Reflecting Methods, Fields und Constructors beschrieben.

Hinweis

getParameterAnnotations() gibt ein [parameterIndex][annotation]-Array zurück — ein inneres Array pro Parameter, auch für Parameter ohne Annotationen (diese inneren Arrays sind dann einfach leer). Die auf Parameter basierende Schleife oben ist meistens übersichtlicher.

Ein ausgearbeitetes Beispiel: ein Mini-Validierungs-Scanner

Das Programm deklariert Laufzeit-Annotationen, markiert @Inherited- und @Repeatable-Fälle, und dann durchläuft ein generischer Scanner die Annotationen einer Klasse, die Annotationen ihrer Methoden und die Annotationen der Methodenparameter — das Grundgerüst eines Validierungs- oder Routing-Frameworks.

java— editable, runs on the server

Was man aus der Ausführung mitnehmen kann:

  • getAnnotation(Service.class) gab einen Live-Proxy zurück, dessen value() "users" zurücklieferte — den im Quellcode geschriebenen Wert. Das Auslesen einer Annotation ist nur der Aufruf ihrer Elementmethoden; das Framework reagiert auf diese Werte (hier wird "users" als Routen-Präfix behandelt). Die Annotation trägt Daten, der Scanner liefert das Verhalten.
  • AdminController meldete @Service als vorhanden, aber nicht deklariert: isAnnotationPresent gab true zurück (geerbt von UserController), während getDeclaredAnnotation null zurückgab. Diese Lücke ist ausschließlich auf die @Inherited-Meta-Annotation zurückzuführen und funktioniert nur, weil @Service auf eine Klasse abzielt — Methoden und Felder erben Annotationen auf diese Weise nie.
  • list.getAnnotation(Role.class) gab null zurück, obwohl zwei @Role-Annotationen direkt im Quellcode stehen. Wiederholbare Annotationen werden vom Compiler in einen Roles-Container gepackt, sodass der Einzel-Wert-Getter sie übersieht; getAnnotationsByType(Role.class) entpackte den Container und gab beide Rollen zurück. Für wiederholbare Annotationen immer getAnnotationsByType verwenden.
  • Parameter-Annotationen waren pro Parameter erreichbar: der tenant-Parameter meldete @NotNull als vorhanden und page nicht. Diese Pro-Parameter-Granularität verwenden Bean-Validation- und Request-Binding-Frameworks, um einzelne Argumente zu validieren oder zu injizieren.
  • getDeclaredAnnotations() auf list zählte zwei Annotationen — @Endpoint und den synthetischen Roles-Container — und bestätigte, dass die zwei @Roles auf Ebene der Klassendatei zu einem Container zusammengefasst wurden. Jede Annotation ohne @Retention(RUNTIME) wäre in dieser Zählung nicht aufgetaucht.

Übung

Übung
Ein Framework markiert Methoden mit einer wiederholbaren Annotation '@Role' (eine Methode kann mehrere haben). Bei einer Methode, die mit '@Role('admin') @Role('editor')' annotiert ist, gibt 'method.getAnnotation(Role.class)' null zurück. Warum, und was sollte stattdessen aufgerufen werden?
Ein Framework markiert Methoden mit einer wiederholbaren Annotation '@Role' (eine Methode kann mehrere haben). Bei einer Methode, die mit '@Role('admin') @Role('editor')' annotiert ist, gibt 'method.getAnnotation(Role.class)' null zurück. Warum, und was sollte stattdessen aufgerufen werden?
Was this page helpful?