REST API Generator

Automatische Generierung einer REST API

Problem: Manuelle Entwicklung einer REST API

Die Entwicklung einer REST API ist zugleich schwierig und langweilig. Schwierig ist es, weil die korrekte Umsetzung der Prinzipien des REST Architekturparadigmas, insbesondere des Hypermedia Prinzips viel Erfahrung und gute Planung erfordert. Langweilig ist es, weil trotz aller Unterschiede der konkreten REST APIs die technische Umsetzung mit Hilfe von Frameworks und Bibliotheken über Präsentations-, Logik- und Persistenzschicht sehr ähnlich und nicht wirklich herausfordernd ist, wenn man es schon ein paar Mal gemacht hat.

Lösung: Modell-getriebene Software-Entwicklung

Da liegt die Idee nah, sich den Aufwand für die Entwicklung einer REST API zu sparen und auf eine Idee zurückzugreifen, die es in der Softwaretechnik schon seit einiger Zeit gibt: modell-getriebene Softwareentwicklung. Die Idee ist, die zu entwickelnde Software in einem Modell zu beschreiben, das auf einer höheren Abstraktionsebene liegt als zum Beispiel ein Java Quelltext. Aus diesem abstrakten Modell wird dann mit Hilfe eines Software-Generators konkreter Quelltext zum Beispiel in der Programmiersprache Java generiert.

Das Prinzip ist vergleichbar zu einem Compiler, der eine Hochsprache (auf einer hohen Abstraktionsebene) in Assembler (auf einer niedrigeren Ebene) übersetzt. Allerdings verlassen wir bei der modell-getriebenen Softwareentwicklung möglicherweise die sogenannte Turing-Vollständigkeit bei Programmiersprachen, also die Möglichkeit, alle möglichen Algorithmen beschreiben zu können. Und das ist auch sinnvoll, denn wir wollen ja gerade in einem Modell nicht mehr alles beschreiben können, sondern nur eine bestimmte Anwendungsdomäne. Aufgrund dieser Beschränkung nennt man solche speziellen Modellierungssprachen auch Domain Specific Languages (DSL).

Domain Specific Languages

Die Domäne, die wir jetzt also betrachten sollten, sind Anwendungen, die mit dem REST Architekturstil beschreibbar sind. Ziel unserer Arbeit ist die Bereitstellung einer DSL zur kompakten Beschreibung solcher Anwendungen. Was sind die elementaren Bausteine einer REST Anwendung? Zunächst machen reden wir über Ressourcen, die Attribute enthalten und über URIs abgerufen werden können. Dann reden wir über das Verhalten an den URI Endpunkten, die wir Zustände nennen. Wir müssen definieren, welche HTTP Verben bei den verschiedenen Endpunkten benutzt werden können und ob zum Beispiel eine Authentifizierung notwendig ist. Schließlich reden wir über die Hyperlinks, mit denen der Server die Clients der API durch die Zustände der Anwendungen führt.

Für die Repräsentation einer DSL gibt es wiederum verschiedene Ansätze. Man kann eine neue formale Sprache mit Hilfe einer Grammatik definieren und dann mit Werkzeugen wie zum Beispiel ANTLR oder xText diese neue Sprache in eine konkrete Programmiersprache wie Java übersetzen. Oder man definiert eine grafische Modellierungsmöglichkeit, vergleichbar zu einem UML Editor, aus dem dann Quelltext generiert wird. Oder man benutzt XML, JSON oder YAML zur strukturierten Beschreibung. Wir haben uns für einen Ansatz entschieden, in dem wir in Java eine DSL als sogenannte Fluent API definieren.

Eine DSL für REST API

Zum besseren Verständnis hier ein Ausschnitt für die Definition einer API zur Verwaltung von wichtigen Terminen. Zunächst müssen wir einige Meta-Informationen definieren:

1
2
3
4
5
6
RestApi.create( )
  .producer( "mycompany" )
  .projectName( "importantdates" )
  .contextPath( "dates" )
  .generateInPackageWithPrefix( "com.example.importantdates" )
  .usingDatabase( "importantdates" ).atHost( "localhost" ).usingUsername( "login" ).andPassword( "password" ).done()

Der Kontextpfad ist das das erste Pfadelement der URIs. Wenn am Ende die REST API auf einem Server www.example.org deployed wird, dann ist die API unter der URI https://www.example.org/dates erreichbar. In der letzten Zeile wird definiert, dass die Termine in einer MySQL Datenbank abgelegt werden sollen. Als nächstes
definieren wir die Ressource Date:

1
2
3
4
5
6
7
.defineResourceWithName( "Date" )
  .withSimpleAttribute( "title" ).asString( )
  .withSimpleAttribute( "description" ).asString( )
  .withSimpleAttribute( "duedate" ).asDate( )
  .allAttributesDefined()
  .andCachingByValidatingEtags()
  .allResourcesDefined()

Ressourcen

Diese Ressource hat drei Attribute und als Caching-Verfahren soll für diese Ressource die Validierungsmethode mit Etags verwendet werden. Alternativ könnten wir an dieser Stelle auch angeben, dass ein Datum zur Validierung verwendet werden soll oder ein Expires Header gesetzt werden soll. Als nächstes definieren wir die Endpunkte der REST API, die wir in unserem Modell Zustände nennen:

1
2
3
4
.states()
  .dispatcherState()
    .named( "StartState" )
    .grantToEverybody().done()

Zustände

Zu Beginn definieren einen Einstiegspunkt, den die Clients abfragen können, um weitere Hyperlinks zu erhalten. Die URI ergibt sich über den Hostnamen und den Kontextpfad. Für die interne Verwendung benötigt jeder Zustand einen Namen. Mit grantToEverybody legen wir fest, dass man ohne Authentifizierung diesen Zustand aufrufen kann. Für den Einstiegspunkt ist natürlich das HTTP Verb GET vordefiniert.

Als nächstes definieren wir einen Zustand, der die Abfrage einer Collection-Ressource ermöglicht:

1
2
3
4
5
6
7
8
9
10
11
12
13
.primaryGetCollection()
  .named( "GetAllDates" )
  .withResource( "Date" )
  .grantToEverybody()
  .withQuery()
    .name( "SelectByTitle" )
    .queryTemplate( "{model.title} like {query.t.concat('%')}" )
    .queryParameters()
      .named( "t" ).withDefaultValue( "" ).asString()
    .doneWithQuery()
    .offsetSize()
      .offsetParameterIsNamed( "offset" ).withDefaultValue( 0 )
      .sizeParameterIsNamed( "size" ).withDefaultValue( 10 )

Diese Collection von Date Ressourcen bietet einen Filter an, der einen URL Parameter mit Namen t verarbeiten kann. Über die Methode queryTemplate definieren wir die Abfrage für die Datenbank, nämlich dass das Attribut title der Ressource in der Datenbank (model) dem Parameter t aus der URL (query) entsprechend muss, wobei wir an diesen Parameter noch das Prozentzeichen als Platzhalter anhängen. Für das seitenweise Abfragen von Ergebnissen definieren wir einen Paging-Mechanismus mit den Parametern offset und size mit entsprechenden Default-Werten. Der Generator sorgt dann später dafür, dass bei Abfragen auch entsprechende Hyperlinks auf die vorherige und nachfolgende Seite in der HTTP Antwortnachricht mitgegeben werden, falls vorhanden.

1
2
3
4
.primaryGetSingle()
  .named("GetOneDate")
  .withResource( "Date" )
  .done()

Hiermit definieren wir einen einfachen Zustand für den Zugriff auf eine einzelne Ressource über Angabe einer Id. Zum Abschluss noch ein Beispiel für eine Zustand zum Erzeugen einer neuen Ressource mit dem HTTP Verb POST. Hier definieren wir mit grantToGroups eine notwendige Authentifizierung.

1
2
3
4
5
.primaryPost()
  .named( "CreateDate" )
  .withResource( "Date" )
  .grantToGroups( "admin" )
  .done()

Transitionen

Schließlich müssen wir noch die möglichen Transitionen zwischen den Zuständen definieren. Beispielhaft an dieser Stelle zeigen wir nur den Übergang vom Einstiegspunkt auf den Zustand zur Abfrage aller Termine.

1
2
3
4
5
.transitions()
  .fromState( "StartState" )
    .asHeaderLink()
    .toState( "GetAllDates" )
    .usingRelationType( "getAllDates" )

Wenn also der Client den Einstiegspunkt mit einer GET Nachricht abfragt, erhält er im Header der Antwortnachricht einen Hyperlink auf die URI zur Abfrage der Collection-Ressource unter Angabe aller möglichen URL Parameter. Damit kann der Client einfach durch Austauschen der Parameter in der URI einfach die neue Anfrage erzeugen und abschicken. Analog müssen wir für alle weiteren Transitionen entsprechende Angaben machen.

Quelltext generieren

Aus diesem Modell erzeugt nun ein Generator Java Quelltext für die Präsentationsschicht, die die eigentliche REST API darstellt, die Businesslogik zur Verarbeitung der einzelnen Anfragen und die Datenbankschicht mit den CRUD Operationen für MySQL. Es werden außerdem Datenbankskripte und POM Dateien für Maven generiert, mit denen das Projekt sofort ohne weitere manuelle Änderungen auf einen Tomcat Container deployed werden kann. Nach den Erfahrungen der letzten Monate zeigt sich eine enorme Zeitersparnis bei der Verwendung eines solchen modell-getriebenen Ansatzes. Genaue Untersuchungen sind schwierig, noch haben wir keinen Vergleich zwischen einer vollständig manuellen Entwicklung einer solchen API und unserem Ansatz gemacht, aber eine solche API wie oben (natürlich vollständig mit allen Zuständen und Transitionen) umfasst weniger als 100 Zeilen Quellcode und lässt sich in ca. 30 Minuten aufschreiben. Das würde wohl kein noch so erfahrener Entwickler auch nur annähernd in dieser Zeit schaffen.