Lange war es in Java nicht möglich eine Annotation an einer bestimmten Position mehrfach anzugeben. Dieses Manko wurde erst in der Java Version 8 durch Repeating Annotations behoben.
Im Beitrag Hamcrest Matcher Generator
wurde die @Hamcrest
Annotation vorgestellt. Diese Annotation gestattet es, für eine angegebene Klasse, Hamcrest Matcher automatisch zu erstellen.
Leider kann in der initialen Version des Generators immer nur eine @Hamcrest
Annotation an einer Klasse stehen.
@Hamcrest(Person.class) class PersonTest {
Eine einfache Art mehrere Klassen zu übergeben, ist die Verwendung eines Arrays als Annotations Attribut.
@Hamcrest(sources={Person.class, Address.class}, onlyPublic=true) class PersonTest {
Mit dieser Variante beschneidet man sich jedoch die Flexibilität bei der Konfigurierung. Denn die weiteren Attribute der Annotation gelten für alle angegebenen Klassen. Besser also die Variante mit mehreren Annotations.
@Hamcrest(source=Person.class, onlyPublic=true) @Hamcrest(Address.class) class PersonTest {
So umgeformt funktioniert der Generator aber nicht mehr. Dies liegt an der Implementierung der Repeating Annotations. Diese Annotations erhalten in ihrer Definition den Verweis auf eine weitere Annotation, die als Container funktioniert.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) @Repeatable(Hamcrests.class) public @interface Hamcrest { Class<?> value(); }
Werden zwei oder mehr Annotationen vom selben Typ an der selben Position verwendet, dann ersetzt sie der Compiler hinter den Kulissen durch die Container-Annotation. Diese enthält dabei die ursprünglichen Annotationen in ihrem Array-Attribut.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Hamcrests { Hamcrest[] value(); }
Da der Annotations Processor nur @Hamcrest
Annotationen verarbeitet und keine @Hamcrests
Annotationen, wird seine process
Methode in dieser Konstellation nicht aufgerufen.
Welche Gründe es auch immer gegeben haben mag, um Repeating Annotations auf diese Art und Weise zu implementieren, sie macht die Arbeit für Entwickler nicht einfacher.
Die Klasse HamcrestMatcherBuilderProcessor
muss nun zwei Annotationen unterstützen, die recht unterschiedlich zu behandeln sind. Bei der @Hamcrest
Annotation können die Ziel-Klassen direkt aus dem value Attribute ausgelesen werden. Bei der @Hamcrests
Annotation müssen zuerst die @Hamcrest
Annotationen aus dem value
Attribute und dann aus deren value
Attributen die Zielklassen ausgelesen werden.
private boolean processAnnotations(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Stream<TypeMirror> hamcrestsSourceTypes = annotations.stream() .filter(a -> Hamcrests.class.getName().equals(a.toString())) .flatMap(h -> processHamcrestsAnnotations(h, roundEnv)); Stream<TypeMirror> hamcrestSourceTypes = annotations.stream() .filter(a -> Hamcrest.class.getName().equals(a.toString())) .flatMap(h -> processHamcrestAnnotations(h, roundEnv)); return Stream.concat(hamcrestSourceTypes, hamcrestsSourceTypes).distinct() .filter(Objects::nonNull).allMatch(this::processSourceTypes); }
Die processAnnotations
Methode delegiert die Suche nach den Ziel-Klassen an die beiden Methoden processHamcrestsAnnotations
und processHamcrestAnnotations
. Am Ende der Methode werden die Ergebnisse zusammengefügt, um Doubletten bereinigt, zur Erzeugung der Matcher-Klassen verwendet.
Innerhalb der beiden Methoden wird mit Unterstützung der Java Stream API und den, von der Annotation Processor API bereitgestellten, Visitor Implementierungen die eigentliche Arbeit verrichtet. Die Details dazu finden sich in den Sourcen auf GitLab.
Wer in Zukunft eigene Repeating Annotations einsetzen sollte, muss immer daran denken, dass hinter der Bühne alles etwas komplizierter ist.