In diesem Blogpost möchte ich gerne meine Erfahrungen aus der Entwicklung eines Microservice basierten Kleinprojekts teilen. Als technische Basis der einzelnen Services kommt CherryPy zum Einsatz.
Während des Projekts bin ich auf mehrere Themen gestoßen, die ich gerne vorstellen möchte, und zu denen es jeweils einen eigenen Blogpost geben wird:
- Teil 1: URL-Routing
- Teil 2: Redis zur Inter-Service-Kommunikation
- Teil 3: CORS mit CherryPy
Meine Erfahrungen, die ich mit CherryPy zum Thema URL-Routing gemacht habe, stelle ich in diesem Blogpost vor.
URL-Routing mit CherryPy
Für jemanden wie mich, der die letzten vier Jahre als Product Owner und weniger als Entwickler gearbeitet hat, sind bereits vermeintliche Basics zu Erfolgserlebnissen geworden. Dazu gehört zunächst das Thema URL-Routing.
Generell braucht es in jedem Service Verbindungen zwischen den API-Endpunkten und entsprechenden internen Funktionen, die die Anfragen an diese Endpunkte verarbeiten, wie z.B.:
API–Endpunkt | Interne Funktion |
---|---|
your.tld/members/{uuid} |
getMemberInformation(uuid) |
In CherryPy gibt es dafür diverse Dispatcher, die diese Verbindung auf verschiedene Arten herstellen:
DefaultDispatcher
RoutesDispatcher
MethodDispatcher
DefaultDispatcher
Wird bei der Konfiguration von CherryPy nicht explizit ein bestimmter Dispatcher gewählt, so kommt der DefaultDispatcher
zum Einsatz. Dieser Dispatcher interpretiert die URL als Baumstruktur. Meine Anwendung könnte also z.B. wie folgt aufgebaut sein:
Um das gezeigte Beispiel nun mit CherryPy umzusetzen, brauchen wir zwei Dinge:
- Eine Klasse, deren Methoden das Handling für
/
,/admin
und/album
bereitstellen - Das Mounting dieser Klasse in CherryPys URL-Baumstruktur
Schauen wir uns zunächst an, wie eine solche Klasse aussehen könnte:
Die Klasse enthält also Methoden, die nach der URL-Struktur benannt sind, mit der Ausnahme von /
, dessen Methode schlicht index
heißt.
Hinweis
Um Zugriffe auf nicht definierte URLs abzufangen, kann die Methode default
genutzt werden, die so gesehen als Catch All fungiert.
Zum jetzigen Zeitpunkt weiß CherryPy noch nicht, auf welcher Ebene index
einzusortieren ist. Dies passiert nun beim Mounting unserer Klasse in CherryPy.
Dieser Befehl sorgt dafür, dass unsere Webapp SimpleWebGallery
im Verzeichnisbaum auf den Root /
gemappt wird, und damit folgendes Mapping hergestellt wird:
API–Endpunkt | Interne Funktion |
---|---|
/ |
SimpleWebGallery().index() |
/admin |
SimpleWebGallery().admin() |
/album |
SimpleWebGallery().album() |
Als Beispiel zur Verdeutlichung
würde folgendes Mapping bewirken:
API–Endpunkt | Interne Funktion |
---|---|
/foo |
SimpleWebGallery().index() |
/foo/admin |
SimpleWebGallery().admin() |
/foo/album |
SimpleWebGallery().album() |
Rufen wir jetzt also http://localhost:8080/
auf, so wird das Ergebnis der folgenden Methode ausgeliefert.
Tiefer in den Baum
Der nächste Schritt ist interessanter. Meist bestehen Bereiche aus mehreren Sektionen, die sich um verschiedene Dinge kümmern, sprich: die Teilbäume sind in der Regel tiefer. In meinem Beispiel müssen Alben und die darin enthaltenen Bilder und Subscriptions administriert werden. Wir haben also folgenden Aufbau:
Wie wir jetzt z.B. an /admin/album/{uuid}
kommen, ist wesentlich spannender, denn hier bietet CherryPy uns zwei Möglichkeiten. Auf diese zwei Arten des Routings möchte ich nur kurz eingehen, da sie zwar ihre Daseinsberechtigung haben, aber aus meiner Sicht den Code nur unverständlicher und schwerer wartbar gemacht haben.
- Weitere Pfadbestandteile als Parameter in der Funktion entgegennehmen.
- Auswerten von
*args
und**kwargs
In Möglichkeit 1 werden alle weiteren URL-Bestandteile als Parameter der entsprechenden Funktion übergeben. Wir müssen also bei der Implementierung antizipieren, wie viele weitere Bestandteile wir erwarten.
Beispiel zu 1. für /admin/album/{uuid}
Die Methode admin
bekommt hier zwei Parameter übergeben, da hinter /admin
zwei weitere URL-Bestandteile folgen. Diese Parameter müssen nun in admin
validiert und ausgewertet werden:
Die Implementierung von admin
kann also beliebig komplex werden, je tiefer sich meine Web-Anwendung verschachtelt.
In Möglichkeit 2 werden die weiteren URL-Bestandteile als Liste *args
an meine Methode admin
übergeben.
Beispiel zu 2. für /admin/album/{uuid}
Allgemein finde ich den DefaultDispatcher
in einfachen Szenarien gut nutzbar, allerdings sind Behandlungen für tiefere Teile der URL unschön.
Zum Schluss ein Komplettbeispiel für den DefaultDispatcher:
MethodDispatcher
Einen ähnlichen Ansatz verfolgt der MethodDispatcher
(MD). Hier wird in CherryPy ein Mounting Tree aufgebaut, der – analog zum Vorgehen beim DefaultDispatcher
– bestimmt, welche Funktionen für welche API-Endpunkte verantwortlich sind.
Im Speziellen nutzt der MD eine explizitere Unterscheidung zwischen den verschiedenen Request-Methoden GET
, POST
, PUT
, DELETE
etc.
Beispiel
In der Variable webapp
wird bestimmt, welche Klassen für welche Teilbäume des URL-Pfades verantwortlich sind. Diese Klassen enthalten dann jeweils die folgenden Methoden:
Hinweis
Im Gegensatz zum DefaultDispatcher
stellen also nicht die Methoden in webapp
die Implementierung des Handling, sondern die Klassen, die den URL-Pfad benannten Attributen zugewiesen sind.
Im gezeigten Beispiel wird ein Mounting Tree
aufgebaut und in der letzten Zeile am Einstiegspunkt /photo-service
eingebunden. Der Service reagiert also entsprechend auf:
Die Besonderheit des MD liegt nun darin, dass die Methoden GET
, POST
, PUT
etc. als explizite Funktionen in den Klassen vorhanden sein müssen und automatisch verbunden werden, also:
API–Endpunkt | Interne Funktion |
---|---|
POST /photo-service/photos |
PhotoServicePhotos.POST() |
GET /photo-service/photos/{uuid} |
PhotoServicePhotos.GET() |
… | … |
Was auf den ersten Blick ziemlich praktisch aussieht, hat im Endeffekt ähnliche Probleme wie unsere erste Variante mit dem DefaultDispatcher
. Eine Klasse oder Funktion ist für einen gesamten Teilbaum verantwortlich, Unterscheidungen bei mehreren Parametern müssen in den Funktionen getroffen werden. Dadurch steckt viel Logik tief im Code, was wenig deklarativ und somit nicht auf den ersten Blick ersichtlich ist.
RoutesDispatcher
Der RoutesDispatcher
erlaubt es uns, Routen anhand von URL-Templates anzugeben und direkt auf eine konkrete Funktion zu leiten. Der große Vorteil ist, dass die weiteren Bestandteile der URL explizit im Code angegeben sind und nicht programmatisch aus Parametern herausgefischt werden müssen. An einem Beispiel wird dies schnell ersichtlich:
Wir können nun für jede URL eine direkte Verbindung zu einer Funktion aufbauen und haben für jeden API-Endpunkt einen konkreten Eintrag im Dispatcher. Dadurch bleibt der Code wesentlich übersichtlicher und damit wartbarer. Weiterhin können variable Anteile in der URL direkt mit {}
kenntlich gemacht werden. Diese werden dann als entsprechend benannte Variablen an die unter action
angegebene Funktion weitergereicht.
Zum Schluss noch ein lauffähiges Minimalbeispiel für den RoutesDispatcher
:
Rufen wir nun lokal http://localhost:8080/user/world
auf, so werden wir mit einem herzlichen “Hello world!” begrüßt.
Fazit
Es sind eine Menge praktische Kleinigkeiten in der Implementierung, die letztlich die meiste Zeit kosten. Eine dieser Kleinigkeiten ist das URL-Routing. CherryPy bietet dazu diverse Wege an, die aber nicht unbedingt alle zu übersichtlichem und wartbarem Code führen. Der RoutesDispatcher
ist zwar leider schlecht dokumentiert, bietet aus meiner Sicht aber aktuell die expliziteste Form des Routing an und fördert damit verständlichen Code.