Entwickler-Blog · 28.08.19

REST Services in Clojure

Im Herbst 2018 sollte ein Prototyp für eine neue E-Beschaffung erstellt und auf dem traditionellen Wintertreffen der MACH AG im Dezember vorgestellt werden. Ziel war es, sowohl eine Designstudie zu schaffen wie auch bestimmte Entwicklungstechnologien zu erproben. Mein Part war es, den Backendservice zu implementieren.

The Beauty of Clojure

Im Herbst 2018 sollte ein Prototyp für eine neue E-Beschaffung erstellt und auf dem traditionellen Wintertreffen der MACH AG im Dezember vorgestellt werden. Ziel war es, sowohl eine Designstudie zu schaffen wie auch bestimmte Entwicklungstechnologien zu erproben. Der Prototyp sollte aus einem Frontendservice bestehen, der das Web UI und die Businesslogik zur Verfügung stellt, und einem Backendservice, der für die Daten zuständig ist und über eine REST Schnittstelle eingebunden wird. Mein Part war es, den Backendservice zu implementieren. Da der Zeitrahmen, auch bedingt durch andere Verpflichtungen, sehr eng war, entschieden wir uns für eine Umsetzung mit Clojure. Der Backendservice war nicht Teil der Technikevaluierung.

Clojure ist eine JVM-basierte Programmiersprache, d. h. sie wird zu Java Bytecode compiliert und ähnelt damit konzeptionell Sprachen wie Scala oder Kotlin. Syntaktisch gehört sie zur Familie der Lisp Sprachen (d. h. "viele Klammern"). Im Gegensatz zu Java ist sie dynamisch typisiert. Clojure steht in der Tradition funktionaler Programmiersprachen, ohne die Strenge rein funktionaler Sprachen zu übernehmen. Über ihr historisches Vorbild Lisp hinaus bietet sie viele moderne Sprachkonzepte wie Immutable Values und Software-transactional Memory (STM). Natürlich ist eine gute Integration mit dem Java Ökosystem selbstverständlich.

All diese Eigenschaften machen Clojure zu einer idealen Sprache, um effizient zuverlässige Services zu entwicklen, doch keine davon war ausschlaggebend, sie für den anstehenden Prototypen zu benutzen. Aber ich hatte ähnliche Software kurz zuvor in Clojure erstellt, war mit den Libraries und den Tools gut vertraut und konnte mit der hohen Entwicklungsgeschwindigkeit rechnen, die ich von Clojure gewohnt war. Dieselbe Aufgabe unter den gleichen Randbedingungen mit Java anzugehen, hätte ich mir zu diesem Zeitpunkt wohl nicht zugetraut.

Da ein Clojure Service ohne Applikationsserver auskommen kann, war das Bootstrapping für den Service in kürzester Zeit erledigt. Ich benutzte das Template von Luminus, um die Projektstruktur zu erstellen und zugleich Support für Swagger anzulegen. Luminus legt auch das Build File an (project.clj), das – ähnlich wie bei Gradle – verschiedene Build Tasks zur Verfügung stellt, u. a. auch eine, um ein .war File zu erzeugen, das sich unter Wildfly deployen läßt. Ich entschied mich aber für den klassischen Ansatz, der ein self-contained .jar File baut, was man sofort auf einer Java Runtime deployen kann, und in dem alles Nötige enthalten ist, inklusive Web Server, Bibliotheken und Clojure Runtime.

REST API Design

Wie für Projekte solcher Art nicht untypisch, gab es keine präzise Spezifikation, eher eine ungefähre Vorstellung über Art und Umfang des Prototypes. Alles andere musste sich "on the go" ergeben, während wir den Code entwickelten. Im agilen Umfeld spricht man hier gerne von "emergentem Design". Das gab mir die Möglichkeit, die REST API nach einer schnellen gemeinsamen Skizze zu entwerfen und direkt zu veröffentlichen, was schnelles Feedback möglich machte – besonders von den beiden Entwicklern, die den Frontendservice implementierten.

So entstanden Endpunkte wie /products für die Liste der bestellbaren Artikel, dann natürlich /orders für die Bestellungen, /orderitems für Bestellartikel, /addresses für die Lieferadressen, aber auch /projects und /cost-units für die Kostenverrechnung. Der Einsatz der Clojure Web Bibliothek Compojure machte es möglich, diese Endpunkte schnell und deklarativ anzugeben und "shallow" zu implementieren, also z.B. für einen GET /orders Request, der die Liste aller existierenden Bestellungen zurückliefert, zunächst einfach eine konstante JSON Liste zu hinterlegen, die der erwarteten Struktur entsprach. So konnte ich die Endpunkte nach und nach tiefer implementieren, hatte aber zu jedem Zeitpunkt eine funktionierende API.

API Dokumentation

Eine große Hilfe bei den schnellen Iterationen der API war auch, dass die Clojure Variante der Swagger Bibliothek direkt von den Compojure Strukturen, plus ein paar zusätzlicher Hinweise, eine komplette API Dokumentation generierte, inklusive der Möglichkeit, jeden REST Endpunkt sofort innerhalb der Dokumentation am Live System ausprobieren zu können. Das erleichterte das Testen und Explorieren der Endpunkte und gab Sicherheit bei den Entwicklern, die die API nutzten. 

Die Clojure Bibliothek Schema erlaubte es, die Datenstrukturen, die bei den verschiedenen REST Requests empfangen oder zurückgegeben wurden, zu beschreiben, ähnlich den Typen in einem statischen Typsystem. Die Übereinstimmung mit diesen Datenspezifikationen wurde dann automatisch durch die Routing Bibliothek überprüft. Gleichzeitig wurden diese Specs dazu benutzt, automatisch eine Beschreibung der entsprechenden Parameter auf der Swagger Seite und einfache Beispieldaten zu generieren, die man direkt für die interaktiven Tests nutzen konnte.

Persistenz

Da die Aufgabe des Backendservices die Datenhaltung war, lag die Frage nach der Persistenz nahe. Luminus unterstützt eine Reihe von Datenbanksystemen, u. a. SQL Datenbanken wie H2 oder Postgres, sowie Non-SQL Systeme wie MongoDB oder Datomic. Aber für den Prototyp konnten wir uns auf eine sehr einfache Lösung einigen: Das System musste beim Programmstart nur ein initiales Datenset einlesen, das sich dann zur Laufzeit modifizieren ließ. Die Notation, die sich dafür anbot, war Clojures EDN, die "Extended Data Notation", eine Art erweitertes JSON. Genauso wie JSON Literale automatisch gültiges JavaScript und Python sind, so sind EDN Literale gültiges Clojure. Ich konnte also einfach die Datenstruktur, die die Nutzdaten während der Laufzeit im Hauptspeicher repräsentiert, von Platte aus einem File einlesen (und hätte sie bei Bedarf auch in gleicher Weise zurück serialisieren können).

Zur Laufzeit wurde diese Datenstruktur dann durch die Benutzer verändert, neue Bestellungen kamen hinzu, existierende wurden verändert, Anhänge wurden hochgeladen etc. Da Daten jeglicher Art bei Clojure grundsätzlich erst einmal unveränderlich sind, wurden all diese Änderungen durch ein Atom kanalisiert, ein spezialisierter Typ in Clojure, der verändernden Zugriff auf ein anderes Datum steuert und damit konkurrierenen Zugriff bietet, der atomar, unkoordiniert (unabhängig von anderen Daten) und synchron ist (blockiert bis die Veränderung manifest ist).  Die Transformation der JSON Daten an der REST Schnittstelle von und nach EDN geschah dabei, man ahnt es schon, automatisch.

Zusammenfassend kann ich sagen, dass dieser Technologie-Stack eine exzellente Grundlage bot, um in einem hoch-dynamischen, verteilten Entwicklungsprojekt unter engen Zeitvorgaben eine solide REST API zu implementieren und zu dokumentieren. Verschweigen will ich aber auch nicht, dass der Einsatz von Clojure nicht nur auf Gegenliebe bei den Kolleg:innen stieß.  All die guten Eigenschaften können eingefleischte Liebhaber:innen statisch getypter ALGOL-Sprachen eben nicht so einfach überzeugen.

MACH Karriere

Komm ins Team

und mach mit uns die öffentliche Verwaltung digital
Du willst deine Superkräfte für eine gute Sache mobilisieren? Dann finde bei uns deine Aufgabe!
Entwickler-Blog
#Performance

Das n+1 Problem

Dr. Jonathan Moebius kümmert sich bei MACH u. a. um die Analyse und Behebung von Performance-Problemen. Was es mit dem n+1 Problem auf sich hat, erklärt er in diesem Artikel.