Vertrag oder nur Dokumentation?

Enterprise Tales: OpenAPI und Enterprise Java

Arne Limburg
Funktionale Programmierung

© S&S_Media

Nachdem wir uns in der Kolumne der Oktober-Ausgabe dem OpenTracing-Standard gewidmet haben, wollen wir uns jetzt die OpenAPI-Spezifikation und deren Integration in das MicroProfile näher anschauen. Wie immer geht es dabei nicht nur darum, welche technischen Möglichkeiten der Standard liefert. Vielmehr wollen wir die Praxistauglichkeit der Spezifikation und die möglichen Anwendungsszenarien kritisch beleuchten und die verschiedenen Alternativen gegeneinander abwägen.

Als Swagger und das Swagger-UI vor Jahren aufkamen, fungierten sie als eine gute und einfache Möglichkeit, die damals ebenfalls recht neuen RESTful APIs zu dokumentieren. Schnell entwickelte sich Swagger zum De-Facto-Standard in Sachen API-Dokumentation.

Da war es nur konsequent, aus der Swagger-Spezifikation heraus einen offiziellen Standard zur APIDokumentation zu entwerfen und zu etablieren. Die OpenAPI-Initiative gründete sich und am 1. Mai 2017 wurde der Release Candidate 0 der OpenAPI-Spezifikation der Version 3.0 veröffentlicht. Der Entschluss, dem ersten Release die Versionsnummer 3.0 zu geben, fiel vor dem Hintergrund, dass zu diesem Zeitpunkt noch niemand abschätzen konnte, inwieweit der Standard von der Community angenommen werden würde. OpenAPI wurde im ersten Schritt lediglich als die Weiterentwicklung von Swagger gesehen, das zu diesem Zeitpunkt in Version 2 vorlag. Mittlerweile hat sich OpenAPI als Standard zur Dokumentation von APIs weitgehend etabliert, unabhängig davon, ob das API im Nachhinein dokumentiert wird (Code-First) oder bereits vor der Implementierung (Contract-First).

API Summit 2019
Thilo Frotscher

API-Design – Tipps und Tricks aus der Praxis

mit Thilo Frotscher (Freiberufler)

Golo Roden

Skalierbare Web-APIs mit Node.js entwickeln

mit Golo Roden (the native web)

Code-First vs. Contract-First

Die Diskussion, ob es sinnvoll ist, zunächst mit dem Code zu starten und daraus dann die API-Dokumentation zu generieren, oder ob es nicht besser ist, die Schnittstelle vorher zu planen, die Dokumentation von Hand mit einem geeigneten Tool zu erzeugen und gegebenenfalls daraus den Code zu generieren, ist älter als OpenAPI und sogar älter als Swagger. Solche Diskussionen hat es bereits in der SOA-Welt gegeben, als Schnittstellen noch mit Hilfe von WSDLs spezifiziert wurden.

Damals wie heute bringen beide Varianten Vor- und Nachteile mit. Sicherlich existieren Mischformen. Beim initialen Aufsetzen der Schnittstelle hängt die Wahl des besten Vorgehens vom Projektkontext ab. Wenn Client und Server initial vom selben Team entwickelt werden, ist die Code-First-Variante normalerweise die Variante, mit der sich schneller Ergebnisse erzielen lassen. Wenn hingegen die Schnittstelle ohnehin zwischen Client und Server abgestimmt werden muss, weil beide von unterschiedlichen Teams entwickelt werden, kann es sinnvoller sein, nicht nach jeder Abstimmungsrunde wieder den Code anpassen zu müssen. Die Alternative lautet, das Ganze zunächst komplett auf Spezifikationsebene durchzuspielen, bevor dann der finale Code generiert oder manuell implementiert wird.

Verdeutlichen wir uns beide Vorgehensweisen, tritt der erste Nachteil der Code-First-Variante zu Tage: Entwickler neigen dabei dazu, die Beschreibungen der Schnittstellenoperationen kurz zu halten. Alles, was sich automatisiert aus dem Code auslesen lässt, erscheint in der Dokumentation. Wenn es aber darum geht, einen längeren beschreibenden Text zu einer Operation zu schreiben, passiert das selten. Das liegt weniger am Unwillen der Entwickler, sondern an der Art, in der solche längeren Texte verfasst werden müssten. Sie müssten in ein Stringattribut in eine Annotation geschrieben werden. Jeder, der es schon einmal versucht hat, wird bestätigen, dass es nicht angenehm ist, einen längeren Text in eine Stringkonstante zu verpacken. Das bessert sich sicherlich mit Java 12. Ab dieser Version wird es die sogenannten Raw String Literals geben.

Eigentlich interessant wird es in der Diskussion Code-First vs. Contract-First aber erst, wenn es darum geht, die spezifizierte Schnittstelle weiterzuentwickeln. Hier haben nämlich beide Varianten Schwächen, mit denen es umzugehen gilt.

Die Code-First-Variante hat das Problem, dass Codeänderungen an der Schnittstelle immer sofort bis zur Beschreibung durchschlagen. Das hat zwar den Vorteil, dass die Dokumentation immer aktuell ist, es birgt aber die nicht geringe Gefahr, dass der Entwickler inkompatible Schnittstellenänderungen durchführt, ohne das zu merken. Das Nachsehen hat dann der Client.

In der Contract-First-Variante besteht das umgekehrte Problem. Lange Beschreibungen sind einfach zu handhaben, weil sie eben nicht im Code, sondern in der Doku enthalten sind. Dafür muss allerdings jede noch so kleine Änderung an der Schnittstelle im Code, etwa wenn Statuscode hinzugefügt wird, zunächst in der Dokumentation nachgezogen werden. Gegebenenfalls ist es sogar notwendig, die Schnittstelle neu zu generieren. In der Praxis führt das leider häufig dazu, dass solche Änderungen gar nicht dokumentiert werden. Zusätzliche Statuscodes werden dann zwar von der Schnittstelle ausgeliefert, der Client hat aber keine Möglichkeit, im Vorfeld herauszubekommen, dass dieser Fall eintreten kann.

Der Ansatz, regelmäßig neu zu generieren, leidet zusätzlich darunter, dass alle Anpassungen, die nach der letzten Generierung manuell vorgenommen wurden, verloren gehen. Zwar gibt es verschiedene Ansätze, mit Round-Trip-Verfahren dieses Problem zu umgehen. Eines davon heißt, Interfaces zu generieren. Anschließend müssen sie nur noch implementiert werden. Aber häufig verlagern diese Verfahren das Problem nur auf die Datenobjekte. Generiert man auch diese, wandert das Problem weiter in eine Mapping-Schicht, die dafür sorgt, dass die generierten Objekte in das tatsächliche Domänenmodell überführt werden. Da solche Schichten häufig toolbasiert implementiert werden, kommt es vor, dass notwendige Änderungen nicht auffallen. In der Konsequenz führt das zu Fehlern, die schwer zu finden sind.

Schnittstellen zu aktualisieren, ist ein aufwendiger Prozess. Beim Blick auf die Microprofile-Spezifikation schauen wir uns ein interessantes Feature an, was beide Ansätze vereinigt.

Mit Hilfe von Swagger übernehmen

Auch mit Swagger als Implementierung des OpenAPI-Standards lassen sich Code-First und Contract-First realisieren. Sowohl mit Spring REST als auch mit JAX-RS ist es möglich, zunächst mit der Implementierung zu starten. Ist eine Implementierung einmal fertiggestellt, lässt sie sich durch Swagger scannen und erhält fast ohne eigenes Zutun eine schon recht umfangreiche Dokumentation des eigenen API. Um Beschreibungen, Fehlercodes usw. zu ergänzen, die der automatische Prozess nicht erkannt hat, lassen sich Swagger Annotations einsetzen. Sie ermöglichen, die komplette Swagger-Dokumentation und mittlerweile auch die OpenAPI-Spezifikation aus dem Code erzeugen zu lassen. Ein Vorteil dieses Vorgehens liegt definitiv darin, dass die generierte Dokumentation aktuell bleibt und immer der aktuellen Implementierung entspricht.

Aber auch die andere Variante ist möglich: Die Generierung des Servers aus einer OpenAPI-Spezifikation. Bei diesem Vorgehen wird zunächst etwa im Swagger-Editor die OpenAPI-Spezifikation erzeugt und zwischen allen Parteien (Serverteam und potenziellen Clientteams) abgestimmt (Contract-First). Ist die Spezifikation finalisiert, lässt sich über das Tool Swagger-Codegen der Servercode erzeugen. Der so generierte Code lässt sich als Startpunkt für die Implementierung des Servers verwenden. Die im vorherigen Abschnitt beschriebenen Probleme löst Swagger allerdings nicht.

Ins Microprofile übernehmen

Durch die Weiterentwicklung von Swagger hin zum OpenAPI-Standard ist die Übernahme in das Microprofile nur folgerichtig. Zunächst führt das bei den Nutzern allerdings zu Verwirrung. Jetzt konkurrieren drei Player im Markt, die verlangen, gut auseinandergehalten zu werden. Da ist einerseits OpenAPI, der das JSON- und YAML-Format zur Spezifikation von APIs standardisiert. Als zweiten Player gibt es weiterhin Swagger, der diesen Standard implementiert und mit eigenen Swagger-Annotations realisiert. Der dritte Player im Bunde heißt Microprofile, das den OpenAPI-Standard übernimmt. Microprofile basiert aber nicht auf den Swagger Annotations. Es definiert zwecks der Unabhängigkeit von Swagger ein eigenes Set an Annotations, um den OpenAPI-Standard zu realisieren. Natürlich existieren verschiedene Implementierungen des Microprofile-Standards.

Sinnvolle Ergänzungen

Welchen Mehrwert bietet die Microprofile-Spezifikation des OpenAPI-Standards? Neben der Anbieterunabhängigkeit, die ein solcher Standard immer mitbringt, wurde in der Microprofile-Specification tatsächlich ein Feature spezifiziert. Es ist in dieser Form neu, das heißt, es ließ sich bisher mit Swagger nicht realisieren. Die textuelle OpenAPI-Spezifikation wird automatisch mit den vorhandenen Annotations gemerged.

Dieses zunächst unscheinbare Feature ist vor allem vor der Debatte zum Thema Code-First vs. Contract-First interessant. Es ermöglicht eine sinnvolle Kombination der beiden Vorgehensweisen.

Der Entwickler kann beispielsweise mit dem Code starten, um schnell eine erste Version des API zu erzeugen und zu implementieren. Darauf basierend lässt sich die YAML-Datei generieren, um damit in die Schnittstellenabstimmung zu gehen. Ist die Schnittstelle einmal gemäß Contract-First abgestimmt, lässt sich der abgestimmte Contract als Basis für die Weiterentwicklung verwenden. Ergänzende Dokumentation wie weitere Fehlercodes anschließend nah am Code über Annotations festhalten, wohingegen ausführliche Schnittstellenbeschreibungen in der YAML bleiben.

Verbesserungspotenzial

Leider gibt es auch bei der Microprofile-OpenAPI-Spezifikation einige Dinge, bei denen noch deutliches Verbesserungspotenzial besteht. So ist bei dem gerade beschriebenen Feature des Mergens von YAML und Annotations in der Spezifikation nicht beschrieben, wie das Mergen im Detail funktioniert. Wie soll sich beispielsweise der Merge-Algorithmus verhalten, wenn ich zwar über Annotations die Summary einer Operation definieren will, die detailiertere Description aber über die YAML? Von einer Enterprise-tauglichen Spezifikation würde ich an dieser Stelle eine ausgiebige Beschreibung erwarten. In der Microprofile-Spezifikation steht lediglich, dass die YAML Vorrang hat. Ob sich dieser Vorrang aber auf Elemente oder Attribute bezieht, bleibt im Dunkeln und wird den Implementierungen überlassen. In der Praxis bedeutet das momentan leider, dass die Summary aus der Annotation ignoriert würde, auch wenn in der YAML keine Summary zu finden ist.

Eine weitere fragwürdige Entscheidung ist die Einführung der @Schema-Annotation in der Microprofile OpenAPI Spec. Erstens handelt es sich um eine Annotation, die für unterschiedlichste Schema-Angaben zum Einsatz kommt, gewissermaßen eine eierlegende Wollmilchsau. Hier wäre es ohne Frage übersichtlicher gewesen, verschiedene Annotations für die verschiedenen Situationen zu verwenden, anstatt einer Annotation mit über dreißig(!) teils unabhängigen Attributen. Viel ärgerlicher ist allerdings, dass eine Integration mit Bean Validation verpasst wurde. So spiegeln viele der dreißig Attribute Aussagen über das Domänenmodell wider, für die eigentlich Bean Validation entworfen wurde, zum Beispiel Länge oder Pattern. Das führt zum Beispiel dazu: wer festlegen will, dass ein String maximal zwanzig Zeichen lang sein darf, muss das Java-Attribut mit zwei Annotations versehen. Die @Schema-Annotation dient dazu, diese Information in die OpenAPI-Definition zu übernehmen. Die @Size-Annotation von Bean-Validation, sorgt dafür, dass die entsprechende Regel zur Laufzeit tatsächlich ausgewertet wird und die Validierung stattfindet.

Weiteres Verbesserungspotenzial der Microprofile-OpenAPI-Spezifikation steckt in der fehlenden Validierbarkeit von YAML und Code. In einem Contract-First-Ansatz liegt die YAML zuerst vor, darauf aufbauend entsteht der Code. Den Entwickler darauf hinzuweisen, wenn der Code nicht mit der YAML übereinstimmt, ergäbe Sinn. Dem Merge-Algorithmus stünden alle Informationen zur Verfügung, um eine solche Aussage zu treffen. Die Spezifikation sieht einen solchen Validierungsmodus leider nicht vor.

Fazit

Die Übernahme des OpenAPI-Standards in das Microprofile ist folgerichtig. Zudem wurde mit dem Feature des Mergens von YAML und Annotations ein sinnvoller Weg geschaffen, um Contract-First und Code-First sinnvoll zu verbinden.

In der Ausgestaltung der Spezifikation hapert es aber an einigen wichtigen Details. Dazu gehört eine detaillierte Definition, wie besagter Merge-Algorithmus arbeiten soll. Auch die mangelnde Integration von Bean Validation ist ein großes Manko.

Wie schon beim Microprofile-Standard zum OpenTracing fällt mein Fazit auch hier gemischt aus. Analog ist es trotzdem möglich, die Mängel der Spezifikation bis zur Aufnahme in eine erste Jakarta-EE-Version zu beheben.

In diesem Sinne, stay tuned.

Geschrieben von
Arne Limburg
Arne Limburg
Arne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: