Der AddressController

https://bildung.social/@oerinformatik/

https://oer-informatik.de/sbb05_addresscontroller

tl/dr; (ca. 14 min Lesezeit): Es wird die eigentliche REST-API für unsere Adressen erstellt: Methoden für PUT, GET, POST und DELETE. Die API wird per PowerShell oder Bash aufgerufen, um sie auszuprobieren. Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt. Weiter geht es dann mit automatischen Tests für den Adress-Controller. (Zuletzt geändert am 04.09.2023)

Um von außen auf das Modell zuzugreifen wollen wir eine ReST-Schnittstelle nutzen. Diese reagiert auf HTTP-Requests (wie etwa Seitenaufrufe in einem Browser) und soll das Modell dargestellt als JSON verarbeiten.

Die HTTP-Requests für unser Address-Modell soll innerhalb unseres SpringBoot-Projekts eine Klasse AddressController verarbeiten.

Erstellen und vorbereiten des AddressControllers

Im Projektexplorer-Arbeitsbereich kann per Kontextmenü auf den Ordner address direkt eine neue Datei erzeugt werden:

New File im Kontextmenü
New File im Kontextmenü

Entsprechend der Namenskonventionen nennen wir den Controller mit vorangestelltem Modellnamen:

Name AddressController.java eingeben
Name AddressController.java eingeben

VSCode erkennt am Namen, dass es sich um eine Java Datei handelt, und fügt zumindest das Package und die Klasse schonmal ein. Andere IDEs (Netbeans, IntelliJ) bereiten deutlich mehr vor. Aber wir wollen ja auch etwas lernen…

Wir ergänzen direkt zwei Annotations für die Klasse: Mit @RestController teilen wir Spring mit, dass sich in dieser Klasse Logik befindet, die HTTP-Requests für einen bestimmten Pfad verarbeitet. Und mit @RequestMapping(path = "/api/address") reichen wir direkt den Pfad nach, für den unser Controller zukünftig verantwortlich sein soll: also für Aufrufe wie z.B.: http://localhost:8085/api/address

Die Klasse wird also wie folgt angepasst:

Bei @RestController handelt es sich um eine zusammengesetzte Annotation aus den beiden Annotationen:

  • @Controller: Kennzeichnen eine @Component, die per Componentscan gefunden wird und zugleich i.d.R. per annotierten RequestHandlern festlegt, wie mit Requests verfahren wird.

  • @ResponseBody: legt fest, dass der Rückgabewert (return ...) der Methoden als Response des Webrequests gewertet werden soll. Wir wollen in diesem Fall auf einen Requests mit den angeforderten JSON-Objekten und dem jeweiligen HTTP-Status antworten.

Vererbungshierarchie der Annotation Component bis zum RestController
Vererbungshierarchie der Annotation Component bis zum RestController

Die geplante Methodestruktur

Wir legen zunächst eine Reihe von Methodenrümpfen an, die wir später noch individualisieren müssen:

  • post(), get(), put(), delete(): vier CRUD-Methoden für die HTTP-Methoden GET, PUT, POST, DELETE mit Zuordnung zu einer id (also scheinbar einer definierten Instanz von Address),

  • list(): eine Methode, die per GET offensichtlich eine Liste aller Adressen zurückgeben soll,

Individualisierung der CRUD-Methoden

Der Zugriff auf das Model wird über das erstellte RepositoryInterface AddressRepository realisiert. Über die @Autowired-Annotation wird dieses per DependencyInjection injeziert. Den Import für die Annotation (org.springframework.beans.factory.annotation.Autowired) sollte automatisch gefunden werden…

Für die Methoden, die die jeweiligen HTTP-Verben behandeln, müssen wir zugehörige Methoden des Registry-Interfaces finden und diese zuweisen. Die Methoden sind hier in der Reihenfolge genannt, die sich am einfachsten testen lässt: erzeugen, lesen, ändern, löschen (create, read, update, delete: CRUD)

post(): die Controllermethode für HTTP-POST (neue Ressource erzeugen)

Per POST-Requests werden neue Ressourcen erzeugt. Der Request erfolgt auf die Klassen-URL (“/api/address”), das wurde ja bereits oben festgelegt. Diese Methode post() soll aber nur auf POST-Requests auf diesen Pfad reagieren. Wir legen das per Annotation @PostMapping fest:

Der Parameter address wird automatisch aus dem Requestbody gebildet - also den Daten, die dem Post-Request angehängt werden. Durch die Annotation @RequestBody prüft das Framework, ob die Übergabe in interpretierbarer Form als JSON erfolgt ist. Wenn dies der Fall ist, dann wird von Zauberhand eine Entität vom Typ Address mit den übergebenen Werten erzeugt.

Als Rückgabewert nutzen wir eine ResponseEntity<>-Instanz. Dies ist eine einfache Klasse, die alles enthält, was unsere HTTP-Anwort beinhaltet: den Wert (den Body/Content der Antwort), den Header und den HTTP-Status.

Die Klassen ResponseEntity<> bietet eine Reihe von Konstruktoren und statischen Methoden, um ein ResponseEntity<>-Objekt zu bauen, an. Die statischen Methoden (wie das von uns genutzte status()) setzten das Builder-Pattern um, dass zunächst ein Builder-Objekt erzeugt, dessen Single Responsibility es ist, eine ResponseEntity<> zu bauen. Dieser Builder (hier: BodyBuilder) bietet weitere Methoden an, mit denen das zu bauende Objekt konfiguriert werden kann, um dann schließlich mit einer finalen Methode (hier body()) das Objekt selbst zurückzugeben.

Im einzelnen sind dies die folgenden Schritte:

  • Die über den RequestBody übergebene Adresse wird im Repository persistiert und das neue Address-Objekt (jetzt mit eingetragener id) in der Variablen savedAddress gespeichert.

  • Der Return-Wert wird per Builder-Pattern zusammengebaut. Das BodyBuilder-Objekt wird erzeugt und die Festlegung des passenden Status-Codes für neu erzeugte Ressourcen (201, CREATED)erfolg. Hierzu wir die Konstante HttpStatus.CREATED übergeben:

  • mit dem von status() zurückgegebenen BodyBuilder-Objekt werden weitere Attribute gesetzt: Es wird festgelegt, dass der Response-Body ein JSON-Objekt sein soll:

    • die Methode gibt erneut ein BodyBuilder-Objekt zurück. Der Aufruf von body() legt fest, dass der Request-Body enthalten soll: das gespeicherte Address-Objekt:

Als ganzes sieht die Methode dann so aus:

Durch die Verkettung der Aufrufe ist die Methode recht übersichtlich geworden. Aber wirklich intuitiv zu verstehen ist Sie vielleicht nicht, wenn man mit dem Builder-Pattern nicht vertraut ist. Es lohnt sich, sich die Klassen ResponseEntity und BodyBuilder an Hand des obigen UML-Diagramms genauer anzuschauen. In welchen Klassen befinden sich die Methoden status(), contentType() und body()? Welche Rückgabewerte haben sie jeweils? Hier kann man eine Menge über das Builder-Pattern lernen!

Falls ein Import nicht gefunden wird: hier ist die komplette Liste:

Dieses Verfahren können wir auch für die anderen Methoden übernehmen.

Manuelles Testen

POST-Requests senden die Daten im HTTP-Body. Wir übertragen die Daten als JSON-Objekt. Wichtig ist, dass wir bei den Requests auch den ContentType und ggf. das encoding/charset anpassen.

Beim Absetzen des Requests mit dem Kommandozeilenprogramm curl für *nix-Betriebssystemen wird der Body über die --data oder -d-Option übergeben. Ein Testaufruf wäre also etwa:

Es wird das geforderte Objekt als JSON zurück gegeben. An Hand der neu zugewiesenen id können wir erkennen, dass das Objekt tatsächlich gespeichert wurde.

HTTP/1.1 201
Content-Type: application/json
Transfer-Encoding: chunked
Date: Tue, 06 Sep 2022 08:20:28 GMT

{"id":1,"firstName":"Hannes","lastName":"Stein","street":null,"streetAdditional":null,"postalCode":null,"city":null,"country":null}

Wer mit Hilfe der PowerShell testen will, muss das Cmdlet Invoke-WebRequest nutzen. Es ist die Entsprechung zu cURL, nutzt aber eine andere Syntax. Die Parameter müssen wie folgt übergeben werden:

Das Ergebnis ist auch hier wie erwartet ein Statuscode 201 (HTTP Created) und eine zugewiesene id.

StatusCode        : 201
StatusDescription :
Content           : {"id":2,"firstName":"Martin","lastName":"Mustermann","street":null,"streetAdditional":null,"postalCode":null,"city":null,"country":null}
RawContent        : HTTP/1.1 201
                    Transfer-Encoding: chunked
                    Keep-Alive: timeout=60
                    Connection: keep-alive
                    Content-Type: application/json
                    Date: Tue, 06 Sep 2022 08:22:02 GMT

                    {"id":2,"firstName":"Martin","lastName...
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Keep-Alive, timeout=60], [Connection, keep-alive], [Content-Type, application/json]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 136

Wunderbar, das Erzeugen von neuen Adress-Einträgen scheint ja schon zu klappen. Nächster Schritt: read!

get() und list(): die Controllermethode für HTTP-GET (Ressourcen lesen)

Es gibt zwei unterschiedliche GET-Requests, die wir behandeln müssen:

  • GET auf den Pfad /api/address soll eine Liste aller Adressen zurückgeben: es ist ein “get all”, implementiert in der Methode list()

  • GET auf den Pfad /api/address/123 mit einer Zahl am Ende soll nur die eine Adresse der betreffenden ID zurückgeben: “get one” , implementiert in der Methode get()

Schneller implementiert ist die “get all”-Variante. Daher fangen wir mal damit an:

list(): GET all

Die list()- Methode ist zuständig für alle GET-Requests auf die Klassen-URL (“/api/address”), daher werden unter der @GetMapping-Annotation keine weiteren Einschränkungen vorgenommen.

Rückgabewert der Methode ist wieder eine ResponseEntity, die diesmal eine Liste der Address-Objekte enthalten soll. Als Rückgabetyp wird also ResponseEntity<Iterable<Address>> festgelegt (das folgt eigentlich erst aus dem folgenden Schritt).

Es soll eine Liste aller Adressen ausgegeben werden. Die Methode findAll() des CrudInterface liefert uns ein generisches Iterable zurück, dass wir für den Typen Address parametriesiert nutzen können:

Jetzt müssen wir (analog zur post()-Methode) die ResponseEntity für die Rückgabe zusammenstellen. Als Status geben wir HttpStatus.OK zurück, im Body geben wir die oben erzeugte Rückgabe der Repository-Methode findAll() an. Dementsprechend sollten wir es anpassen:

Bevor wir das testen können, müssen wir natürlich über POST ein Ressourcen erzeugen. Mit curl in den *nix-Betriebssystemen:

Der eigentliche Aufruf der Methode erfolgt über:

  HTTP/1.1 200
  Content-Type: application/json
  Transfer-Encoding: chunked
  Date: Tue, 06 Sep 2022 09:27:46 GMT

  [{"id":1,"firstName":"Hannes","lastName":"Stein","street":null,"streetAdditional":null,"postalCode":null,"city":null,"country":null},{"id":2,"firstName":"Norbert","lastName":"Nocheiner","street":null,"streetAdditional":null,"postalCode":null,"city":null,"country":null},{"id":3,"firstName":"Zarah","lastName":"Zuletzt","street":null,"streetAdditional":null,"postalCode":null,"city":null,"country":null}]

Die Option -D - gibt auch den ResponseHeader zurück, mit -s werden einige zusätzliche Angaben ausgeblendet. Da GET der Defaultwert ist, kann die Option -X GET weggelassen werden.

In der PowerShell entsprechend: zunächst Testresourcen erstellen:

Und dann den eigentlichen Befehl absetzen:

StatusCode        : 200
StatusDescription :
Content           : [{"id":1,"firstName":"Hannes","lastName":"Stein"}]
RawContent        : HTTP/1.1 200
                    Transfer-Encoding: chunked
                    Keep-Alive: timeout=60
                    Connection: keep-alive
                    Content-Type: application/json
                    Date: Mon, 07 Sep 2020 04:02:10 GMT

                    [{"id":1,"firstName":"Hannes","lastNam...
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Keep-Alive, timeout=60], [Connection, keep-alive], [Content-Type, application/json]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 50

Auch hier ist GET der Defaultwert und die Option -Method 'GET' kann weggelassen werden.

Die PowerShell arbeitet objektbasiert und fügt uns deshalb nur einen kleinen Teaser des Contents in der Übersich an. Wenn wir den gesamten Inhalt sehen wollen, dann müssen wir uns das Attribut Content des Objekts anzeigen lassen. Z.B. so:

oder so

get(): GET one

Wenn mit dem GET-Request zusätzlich noch eine ID übergeben wird, soll nur eine Adresse zurückgegeben werden.

Wir nutzen hierfür die Methode findById() des CrudRepository. Diese gibt uns ein generisches Optional zurück - also konkret ein Optional<Address>. (Für Freigeister: Man hätte das Repository alternativ zum CrudRepository auch mit dem JpaRepository implementieren können, dann würde die Methode getOne() heißen und direkt ein Address zurückgeben. SpringBoot bietet immer mehr als einen Weg an.).

Die get()-Methode muss zunächst wieder annotiert und angepasst werden:

  • @GetMapping("/{id}") identifiziert den Wert, der im aufgerufenen Pfad an Stelle des Platzhalters ({id}) im @GetMapping-Parameters steht als Eingabe. Also den Wert “123” im Pfad /api/address/123.

  • Die Parameter-Annotation @PathVariable long id bindet diesen Wert direkt an unseren Methodenparameter id.

  • Die ResponseEntity wird gebaut mit dem Status HttpStatus.OK, dem gewohnten ContenType JSON und dem Rückgabewert unserer Repository-Methode findById(id):

Es müssen wieder ein paar Imports vervollständigt werden. Hier die passenden Zeilen, falls es nicht automatisch klappt:

Getestet werden kann dieser GET-Request mit curl (Natürlich erst, nachdem - wie oben zu sehen - Datensätze angelegt wurden… vorher wird null zurückgegeben.)

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 07 Sep 2020 04:04:35 GMT

{"id":1,"firstName":"Hannes","lastName":"Stein"}

In der PowerShell entsprechend:

StatusCode        : 200
StatusDescription :
Content           : {"id":1,"firstName":"Hannes","lastName":"Stein"}
RawContent        : HTTP/1.1 200
                    Transfer-Encoding: chunked
                    Keep-Alive: timeout=60
                    Connection: keep-alive
                    Content-Type: application/json
                    Date: Mon, 07 Sep 2020 04:10:46 GMT

                    {"id":1,"firstName":"Hannes","lastName...
Forms             : {}
Headers           : {[Transfer-Encoding, chunked], [Keep-Alive, timeout=60], [Connection, keep-alive], [Content-Type, application/json]...}
Images            : {}
InputFields       : {}
Links             : {}
ParsedHtml        : mshtml.HTMLDocumentClass
RawContentLength  : 48

put(): die Controllermethode für HTTP-PUT (Ressourcen aktualisieren)

Mithilfe von put() sollen vorhandene Ressourcen mit übergebenen Werten aktualisiert werden. Wir müssen dazu zwei Techniken kombinieren, die wir bei post() und get() schon gekennengelernt haben:

Im Gegensatz zu GET, das alle Informationen aus dem HTTP-Header übergibt, erhält PUT die Infos aus dem HTTP-Body. Das muss über Annotationen festgelegt werden:

  • Die id der zu aktualisierenden Ressource soll im Http-Header (dem Pfad, der URL) übergeben werden. Analog zu get() geschieht dies über die Annotaion @PathVariable long id. Der Wert von ìd wird aus dem aufgerufenen Pfad übernommen und dem Parameter id übergeben.

  • Die zu ändernden Daten werden als JSON im Body des Requests gesendet. Analog zu post() wird durch die Annotation @RequestBody Address address der Inhalt des übergebenen JSON-Strings dem annotierten Methodenparameter address übergeben.

Diese Methode wird etwas aufwändiger: theoretisch kann die ID einer zu ändernden Adresse über die URL und über den gesendeten Content übergeben werden. Es wird geprüft:

  • Wurde keine id mit dem JSON-Objekt im Body übergeben wird die id der URL übernommen, wenn möglich.

  • Sind id der URL und des JSON-Objekts identisch, existiert aber dafür kein Eintrag in der DB wird ein Fehler geworfen.

  • Wurde keine id der URL übergeben, jedoch eine per JSON, so wird diese verwendet (derzeit ungeprüft).

  • Falls die übergebenen id per JSON und URL abweichen oder die zu ändernde Ressource nicht existiert wird ebenfalls ein Fehler geworfen.

Die ResponseEntity baut sich wiederum aus Status, Contenttype und dem Rückgabewert der Repository-Methode save(address) auf.

Der manueller Test kann natürlich wieder nur erfolgen, wenn vorher bereits Ressourcen per POST erstellt wurden (siehe oben). Hier angegeben ist jeweils nur der Test für den happy path - probieren Sie ruhig aus, ob auch die oben genannten Sonderfälle wie erwartet funktionieren.

Mit curl:

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 07 Sep 2020 04:50:29 GMT

{"id":1,"firstName":"Andreas","lastName":"Geändert"}

Mit der PowerShell kann so getestet werden:

id firstName lastName
-- --------- --------
 1 Michael   Nochmalanders

delete(): die Controllermethode für HTTP-DELETE (Ressourcen löschen)

Die DELETE-Requests sind dagegen wieder einfach: Wenn eine Ressource mit dieser ID existiert, muss sie gelöscht werden - andernfalls wird eine Exception geworfen.

Die ResponseEntity enthält hier keinen Content und gibt lediglich zurück, dass sie ausgeführt wurde (Statuscode 203). Sie wird daher per noContent() generiert:

Für das manuelle Testen müssen wir natürlich zunächst per POST eine Ressource erzeugen. Entsprechend müssen die Befehle von oben zuvor ausgeführt werden, dann:

Wenigstens der HTTP-Status kann hier kontrolliert werden:

HTTP/1.1 204
Date: Mon, 07 Sep 2020 04:56:00 GMT

Die Powershell erzeugt hierbei gar keine Ausgabe

Ob die Ressourcen wirklich gelöscht wurden kann mit einem anschließenden GET-Request geprüft werden.

Fazit

Der ReST-Controller wurde erfolgreich erstellt und die API kann offensichtlich benutzt werden. Das manuelle Bedienen und Testen ist aber recht mühsam, ein Frontend haben wir noch nicht. Wie also weiter?

Nächste Schritte

Dieser Artikel ist ein Teil der Artikelserie zu einem Adressbuch-SpringBoot-Projekt.

Um von nun an die Funktionsfähigkeit unseres Controllers nicht aus dem Auge zu verlieren sollten wir Automatische Test für den Adress-Kontroller anfügen.


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY-SA 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: Der AddressController” von Hannes Stein, Lizenz: CC BY-SA 4.0. Der Artikel wurde unter https://oer-informatik.de/sbb05_addresscontroller veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/java-springboot/Backend. Stand: 04.09.2023.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: