Blog

Wie Sie Anmerkungen für die Konfiguration verwenden

Maarten Winkels

Aktualisiert Oktober 22, 2025
8 Minuten

In der Java-Welt haben wir uns seit Java 1.5 an Annotationen gewöhnt und verwenden sie. Obwohl es anfangs einige kritische Stimmen gab, denke ich, dass die meisten von uns inzwischen dazu übergegangen sind, Annotationen ausgiebig zu nutzen. Meiner Erfahrung nach werden Annotationen vor allem bei POJO-Domänenklassen verwendet, um Frameworks wie Hibernate, Spring und Seam und viele andere Frameworks so zu konfigurieren, dass sie mit den benutzerdefinierten Objekten korrekt umgehen können. Es gibt so viele verschiedene Ansätze wie Implementierungen. In diesem Blog versuche ich, ein paar der besseren und ein paar der schlechteren Ansätze zu identifizieren. Der Blog ist nicht so sehr als Kritik an den Frameworks gedacht, aus denen die Beispiele stammen, sondern eher als Anleitung für die Gestaltung Ihrer eigenen Anmerkungen, wenn Sie mit dieser Aufgabe konfrontiert werden.

Beste Beispiele: JPA-Annotationen

Die besten Beispiele für die Verwendung von Annotationen für die Konfiguration - sowohl gute als auch schlechte - finden Sie in JPA-Entity-Klassen. [java] @Entity @Table(name="person") public class Person { @Id @GeneratedValue(strategy=SEQUENCE) @Column(name = "personId") private Long id; @Valid @NotEmpty @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "personId", nullable = false) @org.hibernate.annotations.Cascade(CascadeType.DELETE_ORPHAN) @org.hibernate.annotations.IndexColumn(name = "position") private List<Address> addresses = new ArrayList<Address>(); ... } [/java] Was für eine Fülle von Anmerkungen! Und das in so wenigen Zeilen Code. Jeder, der schon einmal mit dieser Art von Code zu tun hatte, wird jedoch bezeugen, dass dieses Beispiel überhaupt nicht übermäßig komplex ist. Sie werden diese Art von Code überall finden. Zunächst einmal möchte ich sagen, dass ich diese Art der Kodierung für äußerst gelungen halte. Unabhängig davon, ob wir die Verwendung von Annotationen in diesem Beispiel für empfehlenswert halten oder nicht, werden Sie, wenn Sie mit Java-Code arbeiten, eine Menge Code wie diesen lesen und schreiben, so dass es unerlässlich ist, ihn zu verstehen. Der Erfolg ist jedoch nur einer der Indikatoren für Qualität und es ist immer gut, darüber nachzudenken, wie Dinge verbessert werden können.

Eine perfekte Bemerkung

Natürlich gibt es keine perfekte Annotation. Aber als allgemeine Faustregel gilt, dass eine gute Anmerkung das kommentierte Element beschreiben und außerhalb des beabsichtigten Kontexts sinnvoll sein sollte. Diese letzte Anforderung ist ein wenig vage. Ich hoffe, sie wird klarer, wenn wir uns einige Beispiele ansehen. Anmerkungen werden sehr oft als bequemes Mittel zur Konfiguration verwendet. Diese Art der Verwendung verstößt gegen die oben genannte Regel für 'perfekte Anmerkungen', ist aber sehr praktisch und weit verbreitet. Die Nachteile dieser Art der Verwendung sind:

  1. Um die Konfiguration zu ändern, muss der Quellcode neu kompiliert werden.
  2. Die Annotationen (und damit die darin enthaltenen Pakete und oft auch der gesamte Code des Frameworks) müssen sich für die Kompilierung und Ausführung im Klassenpfad befinden.
  3. Es kann (normalerweise) nur eine Konfiguration angegeben werden. Die meisten Frameworks erlauben eine andere Art der Konfiguration, um dies zu umgehen.

...aber diese Nachteile bedeuten nicht, dass diese Art der Nutzung in freier Wildbahn weniger erfolgreich war. Ganz im Gegenteil! Es gibt jedoch Möglichkeiten, Frameworks so zu gestalten, dass sie durch Anmerkungen konfiguriert werden können, die von diesen Nachteilen nicht betroffen sind. Lassen Sie uns das obige Beispiel im Lichte dieser Überlegungen betrachten

  1. Die @Entity-Annotation gibt an, dass Instanzen dieser Klasse Daten enthalten sollten, die persistiert werden sollen. Sie beschreibt, wie die Klasse gedacht ist. Außerdem ermöglicht sie es verschiedenen Frameworks, mit ihr zu arbeiten.
  2. Die Annotation @Table ist eine reine Konfiguration. Sie beschreibt nichts über diese Klasse und ist eng an ihren Kontext gebunden: Datenbanken.
  3. Die Anmerkungen @Valid und @NotEmpty beschreiben die Eigenschaften von gültigen Instanzen dieser Klasse. Sie sind jedoch recht eng an ihren Kontext gebunden: die Datenvalidierung.

Für jede der Anmerkungen im Beispiel kann eine solche Analyse durchgeführt werden. Es ist interessant, darüber nachzudenken, wie nützlich die Anmerkungen im Verhältnis dazu sind, wie perfekt sie sind: Die @Entity-Anmerkung ist zwar nahezu perfekt, aber eigentlich überflüssig, da sie ohne die @Table-Anmerkung nie verwendet wird. Die Annotationen @Valid und @NotEmpty sind viel nützlicher: Sie ermöglichen es, sehr leistungsfähige übergreifende Anliegen mit sehr wenig Code, Konfiguration oder Abhängigkeit des Anwendungscodes vom Framework-Code zu implementieren, so dass dieser Code leicht getestet und wiederverwendet werden kann.

Konfigurationsanmerkungsmuster in freier Wildbahn

Das Schlimmste

All dieses Gerede über perfekte Anmerkungen ist schön und gut, aber wohin führt es uns? Vielleicht ist es besser, sich das andere Ende des Spektrums anzuschauen, wo wir die allerschlechtesten Anmerkungen finden: [java] @XmlJavaTypeAdapter(CurrencyAdapter.class) private BigDecimal price; [/java] Diese JAXB-Anmerkung, mit der Sie die Darstellung eines Wertes in XML konfigurieren können, bindet die Implementierung einer Framework-Schnittstelle an das Feld der Domänenklasse, für die wir das Framework konfigurieren wollen. Wir könnten die Schnittstelle genauso gut in der Klasse selbst implementieren!

Besser... Ebene der Indirektion

Ein besseres Muster finden Sie im Seam-Framework für die Konfiguration von Interceptoren. Der folgende Code konfiguriert einen Interceptor um eine Geschäftsmethode herum, um die Zeit zu messen, die für die Ausführung benötigt wird. [java] @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Interceptors(SeamTimingInterceptor.class) public @interface MeasureCalls {} [/java] [java] ... @MeasureCalls public void someBusinessMethod() { ... } ... [/java] [java] @Interceptor(around = { BijectionInterceptor.class, ... SeamInterceptor.class, }) public class SeamTimingInterceptor { @AroundInvoke public Object timeCall(InvocationContext invocation) throws Exception { long t0 = System.nanoTime(); try { return invocation.proceed(); } finally { long dt = System.nanoTime() - t0; // Log time. } } } [/java] Die erste Annotation definiert lediglich eine übergreifende Funktion, die wir in unserer Anwendung implementieren möchten. Wir können sie ganz einfach in unserer Anwendung verwenden, wie das zweite Fragment zeigt. Was das letzte Codefragment, die eigentliche Implementierung des Interceptors, betrifft, so bin ich der Meinung, dass die Entwickler des Frameworks ein wenig zu weit gegangen sind und sich zu sehr an den Annotationen aufgehalten haben: Der Interceptor hätte einfach eine Schnittstelle implementieren können: [java] public interface AroundMethodInterceptor { Object aroundInvoke(InvocationContext invocation); } [/java] Damit wäre der Vertrag für einen Interceptor viel klarer geworden. Es könnte auch helfen, das andere Problem mit dieser Implementierung zu lösen: Die Business-Klasse benötigt die Annotation zum Kompilieren, die Annotation benötigt den Interceptor und der Interceptor benötigt die Framework-Klassen. Aus dieser Perspektive hat die zusätzliche Ebene der Indirektion nicht viel gebracht.

Beste... Klassenpfad-Scans und Schnittstellen

Ein weiterer, recht eleganter Ansatz ist der JAX-RS-Ansatz zur Erweiterung des Frameworks durch Providerund die RESTEasy-Implementierung dafür. Der folgende Code registriert automatisch einen Exception Mapper, der sich in das Framework integriert und zur Behandlung von EJB-Ausnahmen verwendet wird, die von Ressourcenmethoden stammen. [java] @Provider public class EJBExceptionMapper implements ExceptionMapper<EJBException> { @Kontext privaten Anbietern; @Override public Response toResponse(EJBException exception) { while (exception.getCause() != null && exception.getCause() instanceof EJBException) { exception = (EJBException) exception.getCause(); } Exception cause = exception.getCausedByException(); ExceptionMapper delegate = providers.getExceptionMapper(cause.getClass()); if (delegate != null) { return delegate.toResponse(cause); } sonst { return ExceptionUtil.serverError(cause.getMessage()); } } } [/java] Das Gute daran ist, dass das Framework nach der Annotation sucht und die Klasse dann entsprechend der von ihr implementierten Schnittstelle registriert. Ein weiterer interessanter Aspekt ist die Annotation @Context, die im Grunde als Injektionspunkt fungiert.

Erweiterungen für benutzerdefinierte Annotationen

Mit diesem Mechanismus können Sie das RESTEasy-Framework so konfigurieren, dass es benutzerdefinierte Annotationen für eine benutzerdefinierte Serialisierung verarbeitet. Jackson verfügt über einen ähnlichen Mechanismus wie JAXB zur Konfiguration der Art und Weise, wie ein Feld mit der @JsonSerializer-Annotation serialisiert wird. Leider hat dies die gleichen Nachteile. Ein besserer Ansatz kann mit dem Provider-Mechanismus des Jackson-Frameworks selbst implementiert werden. [java] @Provider public class ObjectMapperResolver implements ContextResolver<ObjectMapper> { private AnnotationIntrospector annotationIntrospector; public ObjectMapper getContext(Class<?> type) { return new CustomObjectMapper(); } class CustomObjectMapper extends ObjectMapper { /**

  • Konstruktor. */ public CustomObjectMapper() { super(null, null, null, new SerializationConfig( DEFAULT_INTROSPECTOR, pair(annotationIntrospector,DEFAULT_ANNOTATION_INTROSPECTOR), STD_VISIBILITY_CHECKER, null), new DeserializationConfig(DEFAULT_INTROSPECTOR, pair(annotationIntrospector,DEFAULT_ANNOTATION_INTROSPECTOR), STD_VISIBILITY_CHECKER, null) ); } } } [/java] Die obige Klasse 'versorgt' das Jackson-Framework mit einem benutzerdefinierten ObjectMapper. Dies geschieht automatisch, da die Klasse mit @Provider annotiert ist und ContextResolver<ObjectMapper> implementiert. Jetzt erkennt das RESTEasy-Framework dies als Teil des Frameworks, das einen ObjectMapper bereitstellt, der in einen @Context-Injektionspunkt injiziert wird. Das Jackson-Framework verwendet denselben Injektionspunkt, um einen ObjectMapper zu finden, der für die Serialisierung verwendet werden kann. Wie bringen wir ihn nun dazu, unsere eigenen Anmerkungen zu erkennen? Die Antwort gibt der unten stehende Code, der im benutzerdefinierten ObjectMapper verwendet wird. [java] public class CustomAnnotationInspector extends NopAnnotationIntrospector { private Map<Class<? extends Annotation>, String> serializerComponents = new HashMap<Class<? extends Annotation>, String>(); @Override public Object findSerializer(Annotated am, BeanProperty bp) { if (property != null) { for (Annotation annotation : bp.getMember().getAnnotated().getAnnotations()) { String name = serializerComponents.get(annotation.annotationType()); if (name != null) { return Component.getInstance(name); } } } return super.findSerializer(am, bp); } } [/java] Dies kann wie unten gezeigt verwendet werden, wenn ein Serializer für die @Currency in der seriliazerComponents-Map konfiguriert ist. (Wir verwenden dies in Kombination mit dem Seam-Framework für die Konfiguration). [java] @Target({FIELD, METHOD}) @Retention(RUNTIME) public @interface Currency {} ... @Currency BigInteger price; ... [/java]

    Fazit

    In diesem Blog habe ich eine Reihe von Mustern für die Konfiguration von Frameworks mit Annotationen vorgestellt. Außerdem habe ich einige nützliche Klassen zur Erweiterung der Frameworks RESTEasy und Jackson mit Unterstützung für benutzerdefinierte Annotationen vorgestellt. Ich hoffe, dass diese Beispiele Sie dazu inspirieren, Ihre eigenen perfekten und nützlichen Annotationen zu schreiben.

Verfasst von

Maarten Winkels

Contact

Let’s discuss how we can support your journey.