Annotationen mit Bedacht einsetzen

Java-Annotationen und Software-Engineering: Wolf im Schafspelz?

Jürgen Lampe
© Shutterstock/Moon Light PhotoStudio

Annotationen haben das Potenzial, langfristig die Wartbarkeit von Software empfindlich zu beeinträchtigen. Hinter ihrer einfachen Form kann sich eine faktisch unbeschränkte Funktionalität verbergen, die es sogar ermöglicht, grundlegende Prinzipien der objektorientierten Programmierung außer Kraft zu setzen. Im Beitrag werden einige Aspekte dieser Gefahr genauer dargestellt. Angesichts der inzwischen kaum wieder zurücknehmbaren Entwicklung kann verantwortungsvolle Programmierung nur darin bestehen, Annotationen diszipliniert und mit Bedacht einzusetzen.

Die Einführung von Annotationen in Java 5 hat – weitgehend unbeachtet – eine Entwicklung eingeleitet, deren Konsequenzen nur schwer absehbar sind. Wahrscheinlich wird es niemand ernsthaft in Betracht ziehen, aber rein technisch gesehen ist es kein Problem, jedes Programm in die in Listing 1 gezeigten Formen zu bringen. Die eigentliche Funktion wird dann in speziellen Annotationsprozessoren und Laufzeitbibliotheken verborgen. Selbstverständlich ist das eine sehr extreme Verwendung von Annotationen. Aber sie zeigt, dass es nützlich ist, sich mit diesem Sprachelement genauer zu befassen.

import magic.annotation.handling.Execute;
@Execute
public class Main {}
// oder
@Execute("Hier steht irgendein Text,"
  + "der bestimmt, was gemacht werden soll"
  + " – muss nicht sinnvoll lesbar sein")
public class Main{}

Was sind Annotationen?

Annotationen sind deklarative Sprachelemente. Syntaktisch gesehen haben sie ihre Wurzeln in den „Tagged Paragraphs“ der Dokumentationskommentare, wie sie in der Urversion der Java-Sprachspezifikation (JLS) beschrieben werden (Abschn. 18.4.1ff). Bei der ersten Revision wurden die Dokumentationskommentare aus der Sprache verbannt und der Spezifikation des entsprechenden Werkzeugs (javadoc) zugeordnet. In der dritten JLS-Ausgabe findet sich erstmals der Begriff Annotation: „An annotation is a modifier consisting of the name of an annotation type (§9.6) and zero or more element-value pairs, each of which associates a value with a different element of the annotation type. The purpose of an annotation is simply to associate information with the annotated program element.“

Seitdem sind die Möglichkeiten und die Verarbeitung von Annotationen mit jeder neuen Version erweitert und verändert worden. Gleichzeitig wurde versucht, die Beschreibung zu verallgemeinern: „An annotation is a marker which associates information with a program construct, but has no effect at run time. An annotation denotes a specific invocation of an annotation type (§9.6) and usually provides values for the elements of that type.“

Das ist allerdings nicht gelungen. Die Feststellung „has no effect at run time“ ist nur dann richtig, wenn man sie so interpretiert, dass nicht unmittelbar und in jedem Fall daraus Bytecode entsteht. Wenn man berücksichtigt, dass der Compiler eine Schnittstelle für das Einbinden spezieller Verarbeitungsprogramme bietet, ist sie zumindest stark irreführend. Deutlich wird, dass Annotationen zusätzliche (Meta-)Informationen darstellen, die nicht Bestandteil des Programms sind und keinen unmittelbaren Effekt auf den annotierten Code haben sollen. In der Folge wird sich zeigen, dass diese Begrenzung derart großzügig interpretiert werden kann, dass sie faktisch bedeutungslos ist. Technisch gesehen sind Annotationen Mengen von Name-Wert-Paaren, die bestimmten Elementen (Klassen, Methoden, Variablen usw.) im Code zugeordnet werden können. Dabei sind die zulässigen Namen und Werte beschränkt. Die Einhaltung dieser Beschränkungen wird durch den Compiler kontrolliert. Die Verfügbarkeit (Retention) der angemerkten Metainformationen kann auf drei Ebenen begrenzt werden: Quelltext, Klassendatei und Laufzeit. Die Festlegung der jeweiligen Grenze erfolgt statisch beim Definieren der Annotation. Für die Verarbeitung kann der Java-Compiler dynamisch um Annotationsprozessoren genannte und individuell implementierbare Java-Module erweitert werden.

Aufmacherbild: The designing new connection technology von Shutterstock / Urheberrecht: Moon Light PhotoStudio

[ header = Seite 2: Wofür werden Annotationen verwendet? ]

Wofür werden Annotationen verwendet?

Als Sprachelemente mit sehr einfacher Syntax und ohne Beschränkung möglicher Bedeutungen sind sie äußerst flexibel, sodass Annotationen für ganz unterschiedliche Zwecke verwendet werden können, z. B.:

• Dokumentation, Hinweise (@Deprecated …)
• Fehlervermeidung, Dokumentation (@Override, @FunctionalInterface …))
• Compilersteuerung (@SuppressWarnings …))
• Definition neuer Annotationen (@Retention, @Target…))
• Konfiguration (@GET, @Name …))
• Frameworkfunktionen (@Entity, @Inject …)

Die Vielfalt der möglichen Bedeutungen stellt eine erhebliche Herausforderung beim Lesen annotierten Programmcodes dar. Die einzige Hilfe können dabei sinnvoll gewählte Bezeichnungen bieten. Allerdings sind die Möglichkeiten, inhaltliche Konzepte in eine einzelne Bezeichnung zu fassen, beschränkt. Das funktioniert gut für beispielweise @SuppressWarnings(„deprecation“), aber schon @Entity erfordert ein spezielles Kontextwissen, das deutlich über die Kenntnis der Programmiersprache hinausgeht.
Im Folgenden werden die Annotationen entsprechend ihrer Rolle im Entwicklungsprozess in drei Gruppen eingeteilt. Diese Einordnung ist unabhängig von der jeweiligen Gültigkeit (Retention).

Rolle: Anmerkungen

Das sind ergänzende Informationen, die das Verständnis des Codes erleichtern sollen. Dazu gehören Hinweise, die eine bessere Fehlerkontrolle ermöglichen (@Override) oder überflüssige Warnmeldungen unterdrücken (@SuppressWarnings). Kennzeichnend für diese Gruppe von Annotationen ist, dass sie vollständig aus dem Code entfernt werden können, ohne dass sich dadurch die beschriebene Software ändert. Das schließt nicht aus, dass diese Art der Annotationen als Basis für ergänzende Programme dient, z. B. für solche zur Codeanalyse oder Testfallgenerierung.

Rolle: Konfigurationsinformationen

Konfigurationsinformationen steuern die Einbindung eines Programmteils in ein umfassenderes System. Wie Anmerkungen haben sie keine unmittelbare Auswirkung auf den erzeugten Code. Im Unterschied zu diesen sind sie aber wesentlich für die Funktion des Gesamtsystems. Allerdings ist die Verwendung von Annotationen zu diesem Zweck nicht zwingend, sondern häufig eine Alternative oder Ergänzung zur Konfigurationsverwaltung in separaten Dateien. Diese Konfigurationsdateien weisen fast immer ein XML-Format auf.

Rolle: Spracherweiterung

Annotationen machen aus Java eine erweiterbare Sprache. Zwar sind die Optionen zur syntaktischen Erweiterung auf die Möglichkeiten der Annotationssyntax beschränkt, aber es gibt praktisch keine Einschränkung der möglichen Semantik.
Technisch lassen sich zwei Wege der Erweiterung, die natürlich auch kombiniert verwendet werden können, unterscheiden:

• Modifikation der Übersetzung: Erfolgt durch beim Java-Compiler registrierte Annotationsprozessoren.
• Modifikation des vom Compiler erzeugten Bytecodes: Kann vor, beim oder nach dem Laden der Klassendateien erfolgen, ist nicht an die Verwendung von Annotationen gebunden.

Diese Spracherweiterungen sind u. a. dadurch charakterisiert, dass ihre Bedeutung nicht aus der Kenntnis der Sprachdefinition abgeleitet werden kann. Ohne spezielle Annotationsverarbeitung ist derart annotierter Code nicht lauffähig.

[ header = Seite 3: Syntax und Semantik ]

Syntax und Semantik

An dieser Stelle ist es nützlich, sich des philosophischen Begriffspaars Form und Inhalt zu erinnern. Formalisierung ist der Weg, mit dem Menschen versuchen, Komplexität und Unübersichtlichkeit beherrschbar zu machen. Die Geschichte der Programmiersprachen ist die Geschichte der Suche nach Formalismen, die durch geeignete Regeln die gewaltige Zahl der möglichen Folgen von Maschinenbefehlen behandelbar machen. An der zu Grunde liegenden Semantik, d. h. den Prozessorinstruktionen hat sich vergleichsweise wenig geändert.
Gute Formalismen zeichnen sich durch ein ausgewogenes Verhältnis der Rollen von Syntax und Semantik aus. Die Syntax soll dabei zum einen durch eine eingängige Darstellung das Verständnis erleichtern. Zum anderen sollen durch formale Regeln möglichst viele semantische Fehler ausgeschlossen werden. Je restriktiver die syntaktischen Regeln sind, desto weniger inhaltlich falsche Ausdrücke können damit beschrieben werden. Weil dabei jedoch gleichzeitig die Ausdruckskraft der formalen Sprache abnimmt, ist das richtige Maß der kritische Punkt.
In den meisten verbreiteten Programmiersprachen ist die Balance beider Gesichtspunkte einigermaßen gewährleistet. Java schneidet in dieser Hinsicht besser ab als C++ u. a. durch:

• reduzierte syntaktische Vielfalt – erleichtert (lesend) das Verständnis
• den Typ boolean, der hilft Fehler der Art a=0, wenn a==0 gemeint ist, zu vermeiden
• keine goto-Anweisung, sondern nur break und continue

Annotationen fallen aus diesem ausgewogenen Bereich heraus. Sie haben eine ausgesprochen simple Syntax, die nur elementare Prüfungen zulässt und weitestgehend ohne Bezug zur jeweiligen Bedeutung ist. In dieser Hinsicht sind sie mit XML vergleichbar. Nicht ganz zu Unrecht taucht in Webdiskussionen verschiedentlich der Begriff „XML-Hölle“ auf. Gemeint ist damit die übermäßige Benutzung des XML-Formats für Konfigurationen aller Art, was die Wartung aufwändiger und fehleranfälliger und damit die Verarbeitung kostspieliger macht. Bei ungezügelter und unkritischer Nutzung von Annotationen ist zu befürchten, dass in nicht allzu ferner Zukunft von einer „Annotationshölle“ die Rede sein wird.

Dezentralisierung

Durch Annotierung werden auch inhaltlich zusammengehörende ergänzende Informationen über den Code verteilt. Im Falle der Konfiguration ist die textuelle Nachbarschaft zu den betroffenen Elementen vorteilhaft. Zudem hilft der Compiler, eine Reihe von Schreibfehler zu erkennen, und die sonst oft erforderliche, aber fehlerträchtige Angabe von vollständigen Klassennamen in Konfigurationsdateien kann ganz entfallen. Die Gefahr, dass Code und Konfiguration auseinanderlaufen, wird deutlich reduziert. Dem stehen aber auch gewichtige Nachteile gegenüber:

• Die Konfiguration ist auf viele Quellcodedateien in möglicherweise verschiedenen Verzeichnissen verteilt, wodurch die Wartung erschwert wird.
• Änderungen der Konfiguration sind dann Programmänderungen, die eine Neuübersetzung mit allen Folgeschritten erfordern. (Dieser Nachteil wird relativiert, wenn nur solche Festlegungen durch Annotationen konfiguriert werden, deren Änderung in der Regel auch Codeanpassungen erfordern, z. B. Mapping-Vorgaben.)
• Die Annotationen können die Lesbarkeit des Programmcodes beeinträchtigen. (Dieser Nachteil könnte durch eine IDE, die ein Ausblenden erlaubt, behoben werden.)

Die Diskussion, ob bestimmte Informationen besser eingebettet oder zentral/dezentral ausgelagert verwaltet werden sollten, wird oft dadurch fehlgeleitet, dass für die ausgelagerte Variante nur XML-Formate betrachtet werden. Viele wichtige Kritikpunkte sind aber nur diesem Format und nicht der Auslagerung geschuldet. Verteilte Spezifikationen werfen immer die Frage auf, wie die globale Konsistenz gewährleistet werden kann. Im Allgemeinen ist das zur Entwurfszeit schwierig.

[ header = Seite 4: Spracherweiterung ]

Spracherweiterung

Spracherweiterungen bieten auf der einen Seite die Chance, die Sprache besser an die jeweilige Aufgabe anzupassen, d. h. aus der universellen eine domänenspezifische (DSL – Domain Specific Language) zu machen. Andererseits gilt Sprachentwurf, auch wenn es nur um abgeleitete Sprachen geht, zu Recht als sehr schwierige Aufgabe. Schließlich heißt das, eine formale Theorie für einen Anwendungsbereich zu definieren. Dabei besteht die reale Gefahr, dass die konzeptionellen Fragen gegenüber denen der technischen Realisierung vernachlässigt werden. Annotationen können das noch verschlimmern, weil die Tatsache, dass es sich um Sprachentwurf handelt, verschleiert wird.
Sprache dient der Kommunikation. Bei Programmiersprachen sind im Unterschied zu natürlichen Sprachen zwei unterschiedliche Arten von Partnern beteiligt: Computer und Menschen. Durch die Implementierung einer Spracherweiterung wird sie dem Computer verständlich gemacht. Der schwierigere Teil ist aber, sie anderen Menschen nachhaltig verständlich zu machen. Erweiterungen sind ein Stück Software und dessen Wartung und Weiterentwicklung erfordert dann jeweils Anpassung des Verständnisses. Dadurch wird die Pflege des Codes verkompliziert, der die Erweiterung nutzt. Der Lernaufwand kann dabei so groß werden, dass er spätere Änderung durch andere als die ursprünglichen Entwickler ausschließt. Letztlich sind bisher, von sehr speziellen Ausnahmen (z. B. Forth) abgesehen, alle Versuche mit erweiterbaren Sprachen an den Problemen der langfristigen Wartung gescheitert: „The benefits of Extensible Programming Languages seem obvious in toy examples, but falter for mature languages.“

Code als Dokumentation

Code ist die ultimative Spezifikation dessen, was eine Software macht. So bequem das Annotieren beim Schreiben ist, so unbequem ist es beim Lesen. Der Inhalt erschließt sich nicht aus der Form und die jeweilige Bedeutung kann an ganz unterschiedlichen Stellen definiert sein. Annotationen, die nicht nur Anmerkungen sind, verschlechtern die Lesbarkeit und beeinträchtigen damit die Rolle des Codes als Dokumentation. Dafür gibt es gleich mehrere Gründe:

1. Zum Verstehen muss man das jeweilige semantische Modell kennen. Das Umschalten zwischen den verschiedenen Modellen erfordert mentale Energie und birgt die Gefahr, dass bei der wechselseitigen Interpretation Fehler passieren.
2. Die übliche Abstraktion durch Klassen, Methoden usw. wird durch Annotationen ergänzt. Diese haben aber den Nachteil, dass ihre Schnittstellen weniger exakt definiert sind.
3. Durch Annotationsprozessoren kann der Quellcode vor der Klassendatei-Generierung in fast beliebiger Weise modifiziert werden. Dann bleiben bestenfalls generierte Codeartefakte oder gar nur der Bytecode als Dokumentation dessen, was wirklich ausgeführt wird – keine guten Aussichten für die Wartung.

Selbstverständlich lassen sich die Auswirkungen dieser Nachteile durch gute Dokumentation minimieren. Alle bisherigen Erfahrungen zeigen jedoch, dass es nur äußerst selten gelingt, diesen Zustand über einen längeren Zeitraum beizubehalten.

Objektorientierung

Erweiterbarkeit und Anpassbarkeit sind zwei wichtige Ziele der objektorientierten Programmierung. Kapselung mit definierten Schnittstellen und Vererbung sind dabei die wichtigsten Wege. Annotationen sind eine zusätzliche Dimension der Erweiterbarkeit. Die sich daraus ergebenden Konsequenzen werden anhand des in Listing 2 aufgeführten Codes für einen RESTful Web Service illustriert. Dargestellt sind die Basisklasse und drei davon abgeleitete Unterklassen. Das Liskovsche Substitutionsprinzip besagt, dass Objekte durch Instanzen ihrer Unterklassen ersetzbar sind. Auf das Beispiel angewendet, lassen sich drei unterschiedliche Ergebnisse beobachten:

• Variante A: liefert das erwartete Ergebnis, den geänderten Text im HTML-Format
• Variante B: liefert eine Fehlermeldung 404 – Not found
• Variante C: liefert den Text im Plaintext-Format

Annotationen können dazu führen, dass allgemein akzeptierte Regeln der objektorientierten Programmierung nicht mehr uneingeschränkt gelten. In diesem Fall kann die durch die Annotationen erweiterte Schnittstelle der Klasse durch abgeleitete Klassen in unzulässiger Weise verändert werden. Dazu kommt, dass Annotationen kein generelles Vererbungsverhalten haben und die Spezifikation der @Inherited-Annotation sich nur auf Klassen auswirkt. Im Beispiel hat das nicht leicht zu durchschauende Auswirkungen. So müssen die abgeleiteten Klassen mit @Path annotiert sein, um überhaupt als Serviceprovider akzeptiert zu werden, da @Path nicht als @Inherited definiert ist. @GET ist ebenfalls nicht @Inherited, wird aber offensichtlich in der Basisklasse gefunden. Die Bedingung, dass die @Produces-Annotation nur zusammen mit @GET verwendet werden kann, wird offensichtlich nirgends geprüft. Ihre Verletzung führt zwar zu einem 404-Fehler beim Aufruf, hinterlässt aber sonst keinerlei Spuren (Listing 2).

// ------------- Basisklasse --------
@Path("service") 
public class WebService {
    @GET
    @Produces(MediaType.TEXT_HTML)
    public String getHtml() {
       return "<html><body><h1>Service</body></h1></body></html>";
    }
}
// ------------- Variante A: ok --------
@Path("service")
public class WebServiceA extends WebService {
    @Override
    public String getHtml() {
       return "<html><body><h1>ServiceA</body></h1></body></html>";
    }
}
// ------------- Variante B: 404 -------
@Path("service")
public class WebServiceB extends WebService {
    @Override
    @Produces(MediaType.TEXT_PLAIN)
    public String getHtml() {
       return super.getHtml();
    }
}
// ------------- Variante C: als Text --
@Path("service")
public class WebServiceC extends WebService {
    @Override
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String getHtml() {
       return super.getHtml();
    }
}

Suchtpotenzial

Annotationen sind cool. Sie haben ein Suchtpotenzial. Man kann fast alles mit ihnen machen. Es ist aber nicht so einfach, dass es jeder gleich kann. Wenn es dann funktioniert, sieht es fast wie Zauberei aus und lässt andere anerkennend staunen.
Die Softwareentwicklung wird erheblich durch psychologische Faktoren und Moden beeinflusst, auch wenn nicht so oft darüber gesprochen wird. Das Entwickeln von und mit Annotationen macht einfach Spaß. Man kann sehr schnell vorzeigbare Ergebnisse produzieren. Das gute Gefühl, nach zehn Minuten einen Web Service am Laufen zu haben, verdrängt alle Gedanken daran, ob der Code in zwei oder drei Jahren noch verstanden werden kann.
Annotationen sind ein exzellentes Beispiel für den Fall, dass eine einfache Lösung für ein einfaches Problem so erfolgreich ist, dass diese einfache Lösung danach so lange auf immer größere Probleme angewandt wird, bis sie selbst zum Problem geworden ist.

[ header = Seite 5: Schlussfolgerung ]

Schlussfolgerungen

Annotationen und das, wozu sie verwendet werden können, sollen verteufelt werden. Der Erfolg zeigt, dass es einen breiten Bedarf an ergänzenden Technologien gibt, der durch die originären Sprachmittel von Java nicht befriedigt werden kann. Das Problem besteht darin, dass Annotationen mit der Vielzahl ihrer möglichen Anwendungen hoffnungslos überfrachtet sind. Ihre Ausdruckskraft ist einfach zu mächtig. Aus Sicht des Software-Engineering und der Programmiermethodik besteht die Herausforderung, für die reichen semantischen Möglichkeiten einen passenden syntaktischen Rahmen zu finden. Das ist keine einfache Aufgabe. Man kann nicht auf schnelle Lösungen hoffen. Angesichts des erwähnten Suchtpotenzials ist sogar zu befürchten, dass Annotationen noch für eine Weile die Softwareentwicklung weitgehend ungehindert durchdringen werden. Das wird an manchen Stellen zu Systemen führen, die den Begriff Legacy-Software um eine anspruchsvolle Facette erweitern. Allen, denen Softwarequalität wichtig ist, bleibt nur der Versuch, durch Disziplin bei der Anwendung die nachteiligen Folgen zu begrenzen. Man muss die Gefahren kennen, um ihnen begegnen zu können. Die erste Frage sollte immer lauten: Sind Annotationen der auch auf längere Sicht optimale Weg, eine bestimmte Aufgabe zu lösen?

Beispielsweise ist Codegenerierung oft eine gute Sache. Es muss jedoch deutlich erkennbar sein, wenn eine Java-Datei gar nicht den tatsächlichen Code enthält, sondern eigentlich die Spezifikation des zu generierenden Codes ist. Leider wird stets das .java-Suffix verwendet, sodass auch diese Kennzeichnung nur durch Konventionen ereicht werden kann. Vor allem muss man dem Druck widerstehen, Annotationen zu verwenden, weil das jetzt alle so machen. Für viele Fälle gibt es Alternativen, die zwar manchmal kurzfristig aufwändiger erscheinen und mehr Code erfordern, auf längere Sicht aber einfach besser sind.
Kurz zusammengefasst: Annotationen sind ein problematischer Bestandteil von Java. Sie bergen die Gefahr, durch übermäßigen und unangemessenen Gebrauch die Erzeugung unwartbarer Software zu fördern. Dessen sollte man sich bewusst sein.

Geschrieben von
Jürgen Lampe
Jürgen Lampe
  Jürgen Lampe ist IT-Berater bei der Agon Solutions GmbH in Frankfurt. Seit mehr als 15 Jahren befasst er sich mit Design und Implementierung von Java-Anwendungen im Bankenumfeld. An Fachsprachen (DSL) und Werkzeugen für deren Implementierung ist er seit seiner Studienzeit interessiert.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: