Close Cookie Preference Manager
Cookie Einstellungen
Wenn Sie auf "Alle Cookies akzeptieren" klicken, stimmen Sie der Speicherung von Cookies auf Ihrem Gerät zu, um die Navigation auf der Website zu verbessern, die Nutzung der Website zu analysieren und unsere Marketingaktivitäten zu unterstützen. Mehr Infos
Unbedingt erforderlich (immer aktiv)
Erforderliche Cookies, um grundlegende Funktionen der Website zu ermöglichen.
Made by Flinch 77
Oops! Something went wrong while submitting the form.
Cookie Einstellungen
January 12, 2021

REST Grundlagen

Neben vielen anderen Einsatzfeldern wird Spring Boot häufig zur Umsetzung von Microservices verwendet. Hierbei hat sich der Representational State Transfer, besser bekannt unter dem Akronym REST, zusammen mit JSON als Datenaustauschformat quasi als Standard etabliert.

Wie in meinem früheren Blogpost schon angedeutet, entbrennen unter Fachleuten gerne äußerst emotionale Diskussionen rund um die korrekte Umsetzung von REST und die damit zusammenhängenden Begrifflichkeiten. Bis auf ein paar Präzisierungen lassen wir diese Diskussionen vorerst mal links liegen und konzentrieren uns heute lieber auf die konkrete Umsetzung eines REST Services mit Hilfe von Spring Boot. Etwas wissenschaftlicher formuliert: Im Richardson Maturity Model werden wir uns größtenteils auf Ebene 2 bewegen, kratzen aber etwas an Ebene 3. Gespannt? Dann starten wir durch!

Grundlage: Die Ressourcen

Einfach gesagt ist alles, was innerhalb einer REST Schnittstelle per URL adressierbar ist eine Ressource. Zu abstrakt? Kein Problem, sehen wir uns ein Beispiel an.

Dafür bemühen wir die gute alte Taskliste! Eine Beispielimplementierung gibt es unter https://gitlab.mischok-it.de/open/tasklist zum Download. In der README gibt es ein paar Hinweise zum Starten und zum Anlegen von Testdaten. Führt man den entsprechenden POST Befehl aus, kann man die erste Ressource, in diesem Fall die Liste aller Tasks, abrufen:

$ curl localhost:8080/tasks | json_pp
[
  {
     "title" : "Get started with Spring Boot",
     "done" : false,
     "id" : 16,
     "description" : "Check out H2 as in-memory solution!",
     "links" : {
        "_self" : "/tasks/16"
     }
  }
]

Ich lasse das auf meinem Ubuntu-Rechner im Terminal laufen und nutze json_pp zur Formatierung der Ausgabe. Natürlich könnt ihr genauso gut Tools wie Postman, etc. verwenden.

Aber zurück zur Ausgangsfrage, also zu den Ressourcen. Wir haben hier die erste Ressource (Liste von Tasks) die unter einer definierten URL (localhost:8080/tasks) erreichbar ist. Beim Aufruf wird einfach ein JSON Array von einzelnen Tasks zurückgegeben – in diesem Fall nur mit einem Element, da ich bisher nur einen Beispieldatensatz angelegt habe.

Einen Hauch von Hypermedia gönnen wir uns schon an dieser Stelle:

...
{
  "title" : "Get started with Spring Boot",
  "done" : false,
  "id" : 16,
  "description" : "Check out H2 as in-memory solution!",
  "links" : {
     "_self" : "/tasks/16"

  }
}
...

Jeder Task stellt wieder eine eigene Ressource dar. So ist der Beispieltask mit der ID 16 innerhalb unserer REST Schnittstelle unter dem URI „/tasks/16“ erreichbar. Das verrät uns der mit _self markierte Link innerhalb des Objekts. Probieren wir doch gleich aus, die Ressource abzurufen:

$ curl localhost:8080/tasks/16 | json_pp
{
  "done" : false,
  "title" : "Get started with Spring Boot",
  "links" : {
     "_self" : "/tasks/16"
  },
  "description" : "Check out H2 as in-memory solution!",
  "id" : 16
}

Tatsächlich erhalten wir – Überraschung!! – unseren Beispieltask von der REST Schnittstelle zurück.

Etwas Struktur

Im REST Jargon ist der einzelne Task ein Kindelement der Taskliste. Dies lässt sich bereits aus der URL herauslesen, da /tasks/16 unterhalb von /tasks liegt. An sich klingt das jetzt nach einer Spitzfindigkeit, aber wir wollen ja versuchen, eine korrekte REST Schnittstelle aufzubauen. Da ist es elementar, diese Strukturen ordentlich anzulegen.

Klarer wird das am Beispiel des Anlegens einer neuen Ressource. Hierfür wird in den meisten Anwendungsfällen die HTTP Methode POST verwendet, Ausnahmen sehen wir uns später an. Anlegen wollen wir einen neuen Task, kennen aber natürlich dessen ID (und damit auch die URI, unter der er erreichbar sein wird) noch nicht. Diese wird der Server für uns festlegen. In REST Schnittstellen gilt dann die Regel, dass die anzulegende Ressource per POST an die übergeordnete Ressource geschickt wird. In unserem Fall: Wir wollen eine neue Task-Ressource anlegen, die übergeordnete Ressource ist die Liste unter /tasks, also schicken wir dort unseren POST hin:

curl -v -X POST localhost:8080/tasks -H "Content-Type: application/json" -d '{"title": "Read further Spring Boot posts", "description": "Just open https://www.spring-boot-blog.de/ in your browser!"}'

Diesen Befehl müssen wir kurz auseinandernehmen. Mit -X POST legen wir POST als HTTP Methode fest, das ist noch klar. Wichtig ist, dass wir unserem Server, der die REST Schnittstelle betreibt, mitteilen, in welcher Form wir ihm die zu schreibenden Daten übergeben. Unsere Daten liegen im JSON Format vor, daher übergeben wir den HTTP Header Content-Type: application/json. Und schließlich noch das JSON Objekt, das wir als neue Ressource anlegen wollen. Und zuletzt habe ich noch den Schalter -v für Verbose eingefügt. Hintergrund: Wir werden den Identifier, also die URL der neu angelegten Ressource gleich aus einem HTTP Header der Antwort lesen. Damit diese Header angezeigt werden, benötigen wir die Verbose-Ausgabe.

Dann probieren wir es doch einfach mal aus:

$ curl -v -X POST localhost:8080/tasks -H "Content-Type: applica'{"title": "Read further Spring Boot posts", "description": "Just open https://www.spring-boot-blog.de/ in your browser!"}'

[...]

> POST /tasks HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.58.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 121
>
* upload completely sent off: 121 out of 121 bytes
< HTTP/1.1 201
< Location: /tasks/17
< Content-Length: 0

[…]

Hurra, der Aufruf war erfolgreich! Wir bekommen in der Antwort einen HTTP Status 201, das bedeutet CREATED. Unsere Ressource wurde also innerhalb der REST Schnittstelle angelegt. Wir erinnern uns: Zu jeder Ressource gehört immer auch eine URL. Aber unter welcher ist unsere neue Ressource jetzt erreichbar? Hier hilft uns der Location Header weiter: Wir bekommen hier /tasks/17 als Rückgabe. Probieren wir doch einfach, die Ressource abzurufen:

$ curl localhost:8080/tasks/17 | json_pp
{
  "description" : "Just open https://www.spring-boot-blog.de/ in your browser!",
  "links" : {
     "_self" : "/tasks/17"
  },
  "id" : 17,
  "done" : false,
  "title" : "Read further Spring Boot posts"
}

Auch das hat geklappt! Wir können die Ressource einzeln abrufen, ebenso erscheint sie in der Listen-Ressource:

$ curl localhost:8080/tasks | json_pp
[
  {
  "links" : {
     "_self" : "/tasks/16"
  },
  "title" : "Get started with Spring Boot",
  "description" : "Check out H2 as in-memory solution!",
  "done" : false,
  "id" : 16
  },
  {
  "id" : 17,
  "done" : false,
  "description" : "Just open https://www.spring-boot-blog.de/ in your browser!",
  "links" : {
     "_self" : "/tasks/17"
  },
  "title" : "Read further Spring Boot posts"
  }
]

PUT kann alles

Wer im Englisch-Unterricht gut aufgepasst hat, erinnert sich: das Verb put kann ungefähr alles bedeuten. Schön, dass das auch die HTTP Methode PUT relativ vielseitig ist… Wie sollte sich eine REST Schnittstelle also verhalten, wenn ein Request mit PUT gegen sie gestellt wird?

Nach gängiger Praxis überschreibt die Payload des Requests eines PUT Requests dann die Ressource hinter der URL. An sich so ähnlich wie POST, aber mit dem Unterschied, dass der Identifier klar, also dem Client bekannt ist. Jetzt kommt die angekündigte Vielseitigkeit: Ist die Ressource bisher nicht vorhanden, wird sie neu angelegt. Schauen wir uns das erstmal an um dann noch etwas tiefer in die Theorie einzutauchen.

curl -v -X PUT localhost:8080/tasks/17 -H "Content-Type: application/json" -d '{"title": "Updated title", "description": "Do it with PUT!"}'

Abgesehen davon, dass wir die URL der Einzelressource verwenden, sieht das eigentlich genau so aus, wie der POST zuvor. Es gibt jedoch einen wesentlichen Unterschied: Ein PUT muss immer idempotent sein, ein POST nicht. Wer jetzt raus ist: keine Panik, das schauen wir uns gleich genauer an.

Idempotenz

Neben all den mehr oder weniger zielführenden Diskussionen über REST Schnittstellen gibt es Grundprinzipien der HTTP Kommunikation, deren Befolgung viel Ärger ersparen kann. Dazu gehört zum Beispiel, dass GET Requests den Serverzustand nicht ändern dürfen. Was so selbstverständlich klingt wird erschreckend oft verletzt, auch heute noch. Warum ist das wesentlich? Die ganze HTTP Infrastruktur, Proxies, Caches, etc. verlässt sich darauf, dass ein GET nur liest und nichts verändert. Daher wird hier bei Caching-Strategien logischerweise komplett anders vorgegangen als bei schreibenden Operationen, die immer beim Server landen müssen. Egal ob bei REST Services oder irgendeiner anderen Form von HTTP Kommunikation.

Aber zurück zur Idempotenz. Das Prinzip besagt folgendes: Eine Operation heißt idempotent, wenn ihre mehrmalige Ausführung den gleichen Serverzustand zur Folge hat. Ein HTTP POST Request wird nicht als idempotent erwartet, was auch Sinn macht: Wenn ich zweimal einen neuen Task ohne Identifier anlege, erwarte ich auch zwei angelegte Ressourcen. Anders bei PUT: Da ich den Request hier direkt auf den Identifier (in unserem Beispiel oben /tasks/17) schicke, möchte ich bei zweimaliger Ausführung des gleichen Requests immer noch nur eine Ressource unter der URL haben. An sich eigentlich nicht so schwer.

Bei der Entscheidung, welche Methode bei der Umsetzung einer REST Schnittstelle zum Anlegen neuer Ressourcen zu wählen ist, hilft folgende Daumenregel: Möchte ich, dass der Server eine ID (allgemeiner einen Identifier) generiert, nutze ich POST, wenn ich den Identifier selbst definieren möchte PUT. Für Updates kommt sowieso nur PUT in Frage.

Das bessere PUT

Eine HTTP Methode, die aus meiner Sicht leider immer noch viel zu wenig eingesetzt wird ist PATCH. An sich funktioniert PATCH genauso wie PUT, ist also eine schreibende, idempotente Operation. Der wesentliche Unterschied: Währen PUT immer die gesamte Ressource überschreibt, setzt PATCH nur die tatsächlich übergebenen Felder. Das kann sehr praktisch sein, weil der Client nicht erst die Ressource laden, dann editieren und wieder speichern muss. Man kann einfach die gewünschten Felder mit den neuen Werten an den Server schicken.

Im Beispiel können wir so einzelne Tasks auf erledigt setzen:

$ curl -v -X PATCH localhost:8080/tasks/17 -H "Content-Type: application/json" -d '{"done": true}'

Lese ich danach die Einzel- oder Listen-Ressource, stelle ich fest, dass im Task tatsächlich das Flag done auf den Wert true gesetzt wurde. Wer bis hierhin aufmerksam gelesen hat, bemerkt sicherlich, dass wir das Feld vorhin bei POST und PUT gar nicht explizit gesetzt haben. Genau das ist die Gefahr: ein weiterer PUT ohne das Feld done würde es mit seinem Default belegen, das ist im Fall des Datentyps boolean false. PATCH ist also ein großartiger Weg, eine dynamische REST Schnittstelle zu entwerfen!

Und Tschüss…

Nein, der Post ist noch nicht vorbei ;-) Natürlich können wir Ressourcen auch löschen. Das geht straight-forward per URL und der HTTP-Methode DELETE:

$ curl -v -X DELETE localhost:8080/tasks/17

Im Erfolgsfall ist die Ressource dann weg, unsere REST Schnittstelle liefert beim Aufruf der URL in der Folge einen HTTP Status 404.

Und Spring Boot?

Eigentlich wollte ich etwas mehr auf die Umsetzung mit Spring Boot eingehen, habe mich jetzt doch in der ausführlichen „Einleitung“ verloren... Sparen wir uns die Umsetzung mit Spring Boot einfach für einen weiteren Artikel auf. Es lohnt sich in jedem Fall! Mit der entsprechenden Struktur und den Methoden des Test-Driven-Developments lassen sich standardkonforme REST Schnittstellen mit Spring Boot schnell und sicher umsetzen!

Julius Mischok lächelt in die Kamera.

Danke für's lesen.

Julius Mischok ist Geschäftsführer der Mischok GmbH in Augsburg. Seine Kernaufgaben sind Prozessentwicklung, sowie Coaching und Schulung der Entwicklungsteams. Aktuell fokussiert sich seine Arbeit auf die Frage, wie Software schnell und mit einer maximalen Wertschöpfung produziert werden kann. Julius hat Mathematik studiert und entwickelt seit fast zwei Jahrzehnten Java. Seine Erfahrung brachte er unter anderem in Softwareprojekten für BMW, Audi, Hilti, Porsche, Allianz, Bosch, und viele mehr ein.