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 hereDa 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.
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.
Was man aus der Ausführung mitnehmen kann:
getAnnotation(Service.class)gab einen Live-Proxy zurück, dessenvalue()"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.AdminControllermeldete@Serviceals vorhanden, aber nicht deklariert:isAnnotationPresentgabtruezurück (geerbt vonUserController), währendgetDeclaredAnnotationnullzurückgab. Diese Lücke ist ausschließlich auf die@Inherited-Meta-Annotation zurückzuführen und funktioniert nur, weil@Serviceauf eine Klasse abzielt — Methoden und Felder erben Annotationen auf diese Weise nie.list.getAnnotation(Role.class)gabnullzurück, obwohl zwei@Role-Annotationen direkt im Quellcode stehen. Wiederholbare Annotationen werden vom Compiler in einenRoles-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 immergetAnnotationsByTypeverwenden.- Parameter-Annotationen waren pro Parameter erreichbar: der
tenant-Parameter meldete@NotNullals vorhanden undpagenicht. Diese Pro-Parameter-Granularität verwenden Bean-Validation- und Request-Binding-Frameworks, um einzelne Argumente zu validieren oder zu injizieren. getDeclaredAnnotations()auflistzählte zwei Annotationen —@Endpointund den synthetischenRoles-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.