Blog

'@Composite - Makro-Anmerkungen für Java

Andrew Phillips

Aktualisiert Oktober 23, 2025
9 Minuten

Vor einigen Monaten nahm ich an einer Präsentation teil, bei der Wilfred Springer seine sehr coole Preon Binary Codec Library vorstellte. Die Definition von binären Dateiformaten in Preon erfordert eine ganze Reihe sich ziemlich wiederholender Anmerkungen. In einem Chat nach dem Vortrag erwähnte Wilfred (er hat sogar darüber gebloggt), wie viel bequemer es wäre, wenn man einfach "Abkürzungen" definieren könnte:

@RequiredEnumProperty(Spalte = "AGENT")

für

@NichtNull
@Spalte(name = "AGENT")
@Aufgezählt(EnumType.STRING)

zum Beispiel - und verwenden Sie diese stattdessen. Eine Art "Makro-Annotationen" für Java, wenn Sie so wollen. Ein Gedanke, der vermutlich auch vielen häufigen Benutzern von Hibernate, JAXB oder anderen annotationslastigen Frameworks in den Sinn gekommen ist. Nun, ich habe etwas länger gebraucht als die paar Tage, die ein Entwickler mit Wilfreds Fähigkeiten wahrscheinlich gebraucht hätte, aber endlich strong>@Composite ist da! Um Missverständnisse von vornherein auszuräumen: Hier gibt es keine Bytecode-Verflechtung oder andere Laufzeitmagie, so dass strong>@Composite die Semantik der "normalen" AnnotatedElement-Methoden1 nicht beeinflusst. Zusammengesetzte Annotationen werden stattdessen über eine AnnotatedElements-Schnittstelle unterstützt, die alle bekannten annotationsbezogenen Methoden bereitstellt und registrierte zusammengesetzte Annotationen in ihre "Blatt"-Typen "entpackt". strong>@Composite ist also (noch) keine Drop-in-Magie - Sie müssen die AnnotatedElements-Schnittstelle in Ihrem Code explizit aufrufen.

Hallo zusammengesetzte Welt

Die im Projekt enthaltene Klasse AtCompositeDemo sieht im Wesentlichen wie folgt aus:

public class AtCompositeDemo {
  ...
  // Definieren Sie eine zusammengesetzte Bemerkung
  @Retention(RetentionPolicy.RUNTIME)
  @Target(ElementType.ANNOTATION_TYPE)
  @CompositeAnnotation
  public @interface TargetRetentionLeafCompositeAnnotation {
  boolean runtimeRetention() default false;
  @BlattAnmerkung
  Target targetLeafAnnotation() default @Target({ ElementType.METHOD });
  @LeafAnnotation(factoryClass = RetentionLeafAnnotationFactory.class)
  Retention retentionLeafAnnotation() default @Retention(RetentionPolicy.RUNTIME);
  }
  // Wenden Sie die zusammengesetzte Annotation an...
  @Ressource
  @TargetRetentionLeafCompositeAnnotation(runtimeRetention = true)
  private static @interface AnnotatedAnnotation {}
  // ...an zwei Ziele
  @Ressource
  @TargetRetentionLeafCompositeAnnotation(runtimeRetention = false)
  private static @interface OtherAnnotatedAnnotation {}
  public static void main(String[] args) {
  // holt eine konfigurierte Instanz der AnnotatedElements-Schnittstelle
  AnnotatedElements annotatedElements = ...
  log.info("Abrufen von Annotationen aus AnnotatedAnnotation.class");
  log.info(Arrays.toString(annotatedElements.getAnnotations(AnnotatedAnnotation.class)));
  log.info("Abrufen von Anmerkungen aus OtherAnnotatedAnnotation.class");
  log.info(Arrays.toString(annotatedElements.getAnnotations(OtherAnnotatedAnnotation.class)));
  }
}

Wenn es ausgeführt wird, sollte es eine ähnliche Ausgabe wie die folgende erzeugen

Abrufen von Annotationen aus AnnotatedAnnotation.class
[@javax.annotation.Resource(shareable=true, mappedName=, description=, name=,
type=class java.lang.Object, authenticationType=CONTAINER), @java.lang.annotation.Retention(
value=RUNTIME), @java.lang.annotation.Target(value=[METHOD])]
Abrufen von Anmerkungen aus OtherAnnotatedAnnotation.class
[@javax.annotation.Resource(shareable=true, mappedName=, description=, name=,
type=class java.lang.Object, authenticationType=CONTAINER), @java.lang.annotation.Retention(
value=CLASS), @java.lang.annotation.Target(value=[METHOD])]

die die wichtigsten Merkmale von strong>@Komposit: nämlich, dass die zusammengesetzte Anmerkung korrekt in eine tt>@Ziel und eine tt>@Zurückhaltung Anmerkung, und die "normale" tt>@Ressource Anmerkung wird immer noch aufgegriffen.

Zusammengesetzte Anmerkungen definieren

OK, lassen Sie uns das ein wenig näher erläutern. Eine zusammengesetzte Anmerkung ist einfach eine normale benutzerdefinierte Anmerkung, die selbst mit tt>@CompositeAnnotation versehen ist. Wie jede andere Anmerkung kann sie Mitglieder haben, die u.a. vom Typ Primitiv oder Anmerkung sein können. Zum Beispiel

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
  ...
}

Die "Blatt"-Anmerkungen - die Anmerkungen, zu denen das Composite "expandiert" - sind einfach Mitglieder, die mit tt>@LeafAnnotation annotiert sind. Dies macht natürlich nur Sinn, wenn diese Mitglieder einen Annotationstyp zurückgeben! Beachten Sie, dass die Mitglieder einer zusammengesetzten Annotation nicht automatisch Blätter sind, auch wenn sie eine Annotation zurückgeben.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
  // keine Blattannotation - nicht einmal ein Annotationstyp!
  int numberOfKills();
  // auch keine Blatt-Anmerkung, daher eher sinnlos, sie aufzunehmen
  Abteilung Arbeitgeber Standard @Abteilung("MI6");
  @BlattAnmerkung
  CodeNummer codeNumberLeafAnnotation;
  @BlattAnmerkung
  Freigabe clearanceLeafAnnotation default @Clearance(SECRET);
}

Wie bereits in den Kommentaren erwähnt, ist es zwar nicht verkehrt, Mitglieder, die keine Blatt-Anmerkungen sind, im Composite zu deklarieren, aber es macht im Allgemeinen nicht viel Sinn, da der einzige Zweck des Composites darin besteht, erweitert zu werden. Die Werte, die für die Blatt-Anmerkungen zurückgegeben werden, sind die Werte der Mitglieder der Composite-Instanz, die erweitert wird. Die folgende zusammengesetzte Instanz

@DoubleOhAgentCompositeAnnotation(numberOfKills = 36, codeNumberLeafAnnotation = @CodeNumber("006"))
Agent jackGiddings;

würde zu tt>@CodeNumber("006"), tt>@Clearance(SECRET), und

@DoubleOhAgentCompositeAnnotation(numberOfKills = 103, codeNumberLeafAnnotation = @CodeNumber("007"),
  clearanceLeafAnnotation = @Clearance(TOP_SECRET))
Agent jamesBond;

to tt>@CodeNumber("007"), tt>@Clearance(TOP_SECRET). Die Angabe von guten Standardwerten ist fast immer sinnvoll (zumindest spart es Tipparbeit!), aber wie Sie sehen, können Sie diese bei Bedarf jederzeit überschreiben.

Schreiben von Blattkommentar-Fabriken

Was ist nun mit diesem mysteriösen numberOfKills-Mitglied? Es ist nicht vom Typ Annotation, kann also kein Blatt sein. Außerdem hat es nicht einmal einen Standardwert, d.h. er muss jedes Mal angegeben werden, wenn das Composite verwendet wird! Lästig, oder was? Nun, dazu werde ich noch kommen. Für den Moment sollten Sie bedenken, dass die Definition von Leaf-Annotationen mit Hilfe von Standardwerten zwar schon bequem ist, aber immer noch statisch - die Standardwerte sind in der Definition festgelegt, und selbst das Überschreiben von Standardwerten kann nur mit Werten erfolgen, die zur Kompilierungszeit bekannt sind. Aber was wäre, wenn der Wert einer Leaf-Annotation von einer Laufzeiteigenschaft abhinge - oh, ich weiß nicht, die Tageszeit, die Mondphase? Oder wenn eine nicht-triviale Geschäftslogik involviert wäre, die wir lieber nicht "von Hand" ausführen möchten, wie wir es müssten, um den Wert zur Kompilierungszeit festzulegen? Nicht, dass diese Fälle wahrscheinlich häufig auftreten, aber wenn sie es tun, wäre es sicher schön, den entsprechenden Wert der Blattanmerkung dynamisch generieren zu können. Hier kommt LeafAnnotationFactory ins Spiel2. Sie generiert einen Blattwert auf der Grundlage der zusammengesetzten Anmerkungsinstanz, in der das Blatt deklariert ist - und was immer Sie sonst noch zur Laufzeit in die Finger bekommen können... sogar die Mondphase, wenn Sie möchten. Nehmen wir als Beispiel an, dass wir eine "Gefahreneinstufung" für unsere 00-Agenten berechnen möchten:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
  ...
  @BlattAnmerkung
  Bewertung BewertungLeafAnnotation;
}

Die aktuellen Regeln besagen, dass jeder Agent mit mehr als 100 Tötungen - ah, ja, das numberOfKills-Mitglied - als extrem gefährlich gilt. Natürlich wissen wir, dass Maj. Jack Giddings unter und Bond über dem Limit liegt, also könnten wir die Werte einfach zur Kompilierzeit festlegen.

@DoubleOhAgentCompositeAnnotation(numberOfKills = 36, codeNumberLeafAnnotation = @CodeNumber("006"),
  ratingLeafAnnotation = @Rating(DANGEROUS))
Agent jackGiddings;
@DoubleOhAgentCompositeAnnotation(numberOfKills = 103, codeNumberLeafAnnotation = @CodeNumber("007"),
  clearanceLeafAnnotation = @Clearance(TOP_SECRET),
  ratingLeafAnnotation = @Rating(EXTREMELY_DANGEROUS))
Agent jamesBond;

Was aber, wenn die Regierung Ihrer Majestät in einem Anfall von politischer Korrektheit die Grenze auf 30 senkt? Werden wir daran denken, die Anmerkungen anzupassen? Werden wir das für alle unsere Agenten tun wollen? Nein, es ist besser, den richtigen Wert für ratingLeafAnnotation zur Laufzeit zu generieren.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@CompositeAnnotation
public @interface DoubleOhAgentCompositeAnnotation {
  int numberOfKills();
  ...
 
/*
  * Die Voreinstellung wird *ignoriert* - die Fabrik wird immer aufgerufen - aber es ist mehr
  * Es ist praktisch, sie anzugeben, um zu verhindern, dass der Compiler nach einer solchen Frage fragt.
  */
  @LeafAnnotation(factoryClass = RatingLeafAnnotationFactory.class)
  Rating ratingLeafAnnotation default @Rating(DANGEROUS);
}
Klasse RatingLeafAnnotationFactory
  implementiert LeafAnnotationFactory {
  private static final int EXTREME_DANGER_THRESHOLD = 100;
  public Bewertung newInstance(
  DoubleOhAgentCompositeAnnotation declaringCompositeAnnotation) {
  return RuntimeConfiguredAnnotationFactory.newInstance(Rating.class,
  MapUtils.toMap("Wert",
  (declaringCompositeAnnotation.numberOfKills()  >  EXTREME_GEFÄHRDUNG_SCHWELLE)
  ? Bewertung.EXTREM_GEFÄHRLICH
  : Rating.DANGEROUS));
  }
}
// @Bewertung berechnet auf der Grundlage der Anzahl der Kills
@DoubleOhAgentCompositeAnnotation(numberOfKills = 36, codeNumberLeafAnnotation = @CodeNumber("006"))
Agent jackGiddings;
@DoubleOhAgentCompositeAnnotation(numberOfKills = 103, codeNumberLeafAnnotation = @CodeNumber("007"),
  clearanceLeafAnnotation = @Clearance(TOP_SECRET))
Agent jamesBond;

Jetzt müssen wir nur noch den EXTREME_DANGER_THRESHOLD in der factory3 ändern! Beachten Sie, dass die (vielleicht etwas kontraintuitiv) definierte Voreinstellung für ratingLeafAnnotation keine Auswirkungen auf den Wert hat: die factory wird immer aufgerufen. Aber ohne eine Voreinstellung wird der Compiler bei jeder Verwendung des Composites nach einem Wert für das Mitglied fragen.

Richtlinien für die Verwendung

Damit Sie nicht auf die Idee kommen, sich auszumalen, was man damit alles anstellen könnte, hier ein paar ernüchternde Hinweise:

  • Bedenken Sie zunächst, dass @Composite derzeit nur funktioniert, wenn die Reflexion von Anmerkungen über die Schnittstelle AnnotatedElements erfolgt. Class.getAnnotation(...) wird hier nicht funktionieren!
  • Das bedeutet auch, dass @Composite leider nicht für Hibernate, Spring oder andere Frameworks funktioniert, die intern "normale" Methoden für den Zugriff auf Annotationsinformationen verwenden.4
  • Aus den gleichen Gründen sollte @Composite nicht verwendet werden, um Annotationen bereitzustellen, die von Java selbst verwendet werden, z.B. @Target oder @Retention, da diese für den Compiler nicht sichtbar sind! Kopieren Sie also bitte nicht die Demo ;-)

Beschränkungen

Abgesehen von den oben genannten Richtlinien gibt es eine Reihe von absichtlichen Einschränkungen bei der Verwendung von Composites, die sicherstellen sollen, dass strong>@Composite es Ihnen nicht ermöglicht, Einschränkungen bei der Verwendung "regulärer" Annotationen zu umgehen. Dadurch wird sichergestellt, dass die Semantik der Annotationen mit regulärem Java konsistent bleibt. Das bedeutet unter anderem, dass..:

  • Außerdem darf es nur eine Blattanmerkung eines bestimmten Typs pro Kompositum geben.
  • Das Ziel einer Blattanmerkung muss mit dem der zusammengesetzten Anmerkung übereinstimmen, in der sie deklariert ist.
  • Es darf nicht mehr als eine Anmerkung desselben Typs zu einem bestimmten Element geben, unabhängig von den regulären und zusammengesetzten Anmerkungen zu diesem Element.

Die vollständige Liste finden Sie unter LeafAnnotation und CompositeAnnotation.

Verwendung des Validierungsprozessors

In den meisten der oben genannten Fälle schlägt strong>@Composite frühzeitig fehl, nämlich in dem Moment, in dem die AnnotatedElements-Instanz erstellt wird. Dennoch wäre es schöner, wenn ungültige Konfigurationen - und wie wir gesehen haben, gibt es eine ganze Reihe von Möglichkeiten, solche zu erstellen - erkannt werden könnten, bevor wir überhaupt zur Laufzeit kommen.Um dies zu erreichen, enthält strong>@Composite einen Validierungsprozessor, der ein Java 6 Annotationsprozessor5 ist. Wenn Sie mit Java 6 arbeiten, wird der Prozessor automatisch zur Kompilierungszeit6 ausgeführt und schlägt mit hoffentlich nützlichen Fehlermeldungen fehl, wenn strong>@Composite nicht korrekt verwendet wird. Sie können die Validierungsverarbeitung auch zu Ihrem Eclipse-Projekt hinzufügen - Details dazu finden Sie im Javadoc des CompositeAnnotationValidationProcessor. Eine Demonstration des Validierungsprozessors finden Sie im Projekt at-composite-validator-demo (aphillips.googlecode.com/svn/at-composite-validator-demo/trunk/) und versuchen Sie, es zu bauen!

Integration mit Spring

strong>@Composite wäre kein anständiges Java-Projekt, wenn es nicht irgendeine Art von Spring-Integration bieten würde, oder7? Zum Glück gibt es hier nicht viel zu tun - erstellen Sie einfach eine Instanz von AtCompositeAnnotatedElements (das ein Singleton sein kann) und übergibt dabei eine Liste der zusammengesetzten Anmerkungsarten, die unterstützt werden sollen. AtCompositeDemo demonstriert bereits, wie strong>@Composite mit Spring verwendet werden kann. Weitere Informationen finden Sie in der Javadoc von AtCompositeAnnotatedElements.

@Komposit bekommen

Der strong>@Composite Quellcode und der für nicht standardmäßige Abhängigkeiten ist bei Google Code verfügbar.

Maven

Wenn Sie Maven verwenden, ist die entsprechende Abhängigkeit

  
  com.qrmedia.pattern
  at-composite
  1.0-SNAPSHOT

und Sie müssen das folgende Repository zu Ihrem POM hinzufügen.

  
  qrmedia-freigaben
 
https://qrmedia.com/mvn-repository

Fußnoten
  1. Obwohl das wahrscheinlich ein interessantes Folgeprojekt wäre!
  2. Ich habe sein Erscheinen in der Demo geflissentlich ignoriert, aber ich bin mir ziemlich sicher, dass es mir nicht gelungen ist, es an Ihnen allen vorbeizuschmuggeln ;-)
  3. RuntimeConfiguredAnnotationFactory ist nur ein bequemer Weg, um eine Runtime-Annotation-Instanz zu erzeugen, aber es lohnt sich, einen Blick darauf zu werfen... und sei es nur, um zu sehen, dass es wahrscheinlich einfacher ist, als man vielleicht erwartet!
  4. Ich vermute jedoch, dass es nicht allzu schwer wäre, solche Frameworks so anzupassen, dass sie @Composite verwenden, da es vollständig "rückwärtskompatibel" mit regulären Java-Annotationen ist. Mit einem geschickten Einsatz von Aspekten könnte es sogar eine Möglichkeit geben, dies in bestehenden Code "nachzurüsten".
  5. Das Testen des Anmerkungsprozessors erwies sich als nicht ganz so einfach, wie ich es mir zunächst vorgestellt hatte, aber das ist eine Geschichte für einen anderen Blogbeitrag.
  6. Sie können die Verarbeitung von Anmerkungen unterdrücken, indem Sie dem Compiler das Argument -proc:none übergeben. Siehe die javac-Dokumente.
  7. Obwohl ich zugeben muss, dass ich in letzter Zeit heimlich zu Guice tendiere ;-)

Verfasst von

Andrew Phillips

Contact

Let’s discuss how we can support your journey.