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:

Entsprechend der Namenskonventionen nennen wir den Controller mit vorangestelltem Modellnamen:

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:
package de.csbme.ifaxx.addressbook.address;
@RestController
@RequestMapping(path = "/api/address")
public class AddressController { /* */ }
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.

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 VariablensavedAddress
gespeichert.Der Return-Wert wird per
Builder
-Pattern zusammengebaut. DasBodyBuilder
-Objekt wird erzeugt und die Festlegung des passenden Status-Codes für neu erzeugte Ressourcen (201
,CREATED
)erfolg. Hierzu wir die KonstanteHttpStatus.CREATED
übergeben:mit dem von
status()
zurückgegebenenBodyBuilder
-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 vonbody()
legt fest, dass der Request-Body enthalten soll: das gespeicherteAddress
-Objekt:
- die Methode gibt erneut ein
Als ganzes sieht die Methode dann so aus:
@PostMapping
public ResponseEntity<Address> post(@RequestBody Address address) {
Address savedAddress = addressRepository.save(address);
return ResponseEntity
.status(HttpStatus.CREATED) // erzeugt den BodyBuilder
.contentType(MediaType.APPLICATION_JSON) // ändert den BodyBuilder und gibt ihn wieder zurück
.body(savedAddress); // erzeugt die ResponseEntity und gibt diese zurück
}
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:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
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 Methodelist()
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 Methodeget()
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:
@GetMapping
public ResponseEntity<Iterable<Address>> list() {
Iterable<Address> addressList = addressRepository.findAll();
return ResponseEntity
.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(addressList);
}
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 Methodenparameterid
.Die
ResponseEntity
wird gebaut mit dem StatusHttpStatus.OK
, dem gewohnten ContenType JSON und dem Rückgabewert unserer Repository-MethodefindById(id)
:
@GetMapping("/{id}")
public ResponseEntity<Optional<Address>> get(@PathVariable long id) {
Optional<Address> addressResult = addressRepository.findById(id);
return ResponseEntity
.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(addressResult);
}
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 zuget()
geschieht dies über die Annotaion@PathVariable long id
. Der Wert vonìd
wird aus dem aufgerufenen Pfad übernommen und dem Parameterid
ü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 Methodenparameteraddress
ü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 dieid
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.
@PutMapping("/{id}")
public ResponseEntity<Address> put(@PathVariable long id, @RequestBody Address address) {
if ((address.getId() == null) || (address.getId() == 0)) {
if (id != 0) {
address.setId(id);
} else {
throw new RuntimeException("Es wurde kein zugehöriges Address-Objekt zum Updaten gefunden!");
}
} else if (address.getId() == id) {
if (!(addressRepository.existsById(id))) {
throw new RuntimeException("Es wurde kein zugehöriges Address-Objekt zum Updaten gefunden!");
}
} else if ((id == 0) && (((address.getId() != 0)) || (address.getId() != null))) {
id = address.getId();
} else if ((address.getId() != id) || !(addressRepository.existsById(id))) {
throw new RuntimeException("Es wurde kein zugehöriges Address-Objekt zum Updaten gefunden!");
}
Address savedAddress = addressRepository.save(address);
return ResponseEntity
.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(savedAddress);
}
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:
@DeleteMapping("/{id}")
public ResponseEntity<Address> delete(@PathVariable long id) {
if (!(addressRepository.existsById(id))){
throw new RuntimeException("Es wurde kein zugehöriges Address-Objekt zum Löschen gefunden!");
}
addressRepository.deleteById(id);
return ResponseEntity
.noContent()
.build();
}
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.
Links
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]