Repeating Annotations

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.