REST-API mit Spring¶
Zur Erstellung einer REST-API mit Spring Boot verwenden wir zur Initialisierung des Projektes den Spring Initializr (können Sie aber auch direkt in IntelliJ Ultimate machen: File -> New -> Project -> Spring Initializr) und binden zunächst folgende Abhängigkeiten in unser Projekt ein:
Klicken Sie auf Generate
und speichern Sie die rest.zip
in Ihrem Projekte-Ordner und entpacken die Datei dort.

Erstellen einer PostgreSQL-Datenbank¶
Hier wird gezeigt, wie Sie den HTW-Server ocean.f4.htw-berlin.de verwenden und anbinden können. Sie können PostgreSQL aber auch einfach lokal installieren (siehe hier).
Rufen Sie ocean.f4.htw-berlin.de auf und erstellen sich eine Datenbank, hier rest_jf
. Klicken Sie die neu erstellte Datenbank an und wählen dann den Tab Users
. Erzeugen Sie dort mithilfe von Add new user
einen neuen Nutzer für die Datenbank, hier rest_jf_rest_user
. Lassen Sie sich das generierte Passwort anzeigen:

Öffnen Sie das Projekt in IntelliJ. Wenn Sie sich bei JetBrains mit Ihrer HTW-E-Mail-Adresse anmelden, erhalten Sie kostenlos die Ultimate-Version der IDEA.
Im Ordner src/main/resources
finden Sie die applications.properties
. Öffnen Sie diese und fügen Sie die Konfigurationsdaten für Ihre Datenbank auf Ocean hinzu:
spring.application.name=rest
spring.datasource.url=jdbc:postgresql://psql.f4.htw-berlin.de:5432/rest_jf
spring.datasource.username=rest_jf_rest_user
spring.datasource.password=AhzBkc6m0
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
Für die Eigenschaft spring.jpa.hibernate.ddl-auto
haben Sie 5 Werte zur Auswahl:
create
– Beim Start Ihrer Anwendung werden alle Tabellen, die von Hibernate verwaltet werden, gelöscht und gänzlich neu kreiert.create-drop
– Beim Start Ihrer Anwendung werden alle Tabellen, die von Hibernate verwaltet werden, neu kreiert und wenn die Anwendung geschlossen wird, werden die Tabellen gelöscht.update
– Beim Start der Anwendung werden die bereits existierenden Tabllen an die Schemata von Hibernate angepasst, wenn nötig.validate
– Beim Start der Anwendung wird geprüft, ob die existierenden Tabellen mit den Schemata von Hibernate übereinstimmen. Wenn nicht, wird eine Exception geworfen.none
– Es wird keine automatische Schemata-Verwaltung durchgeführt.
Es ist nicht gut, die sensitiven Daten, wie z.B. password
in Klarsicht in der application.properties
abzulegen. Sie sollten sich dazu eigene Umgebeungsvariablen erstellen, z.B. OCEAN_PASSWORD
und können dann in der application.properties
per ${OCEAN_PASSWORD}
darauf zugreifen. Siehe z.B. external configuration.
Sie können Ihre Anwendung jetzt bereits einmal starten. Es sollten keine Fehler auftreten.
Eine Java-Entität mit Hibernate erstellen¶
Wir wollen eine Entität User
erstellen. Diese soll folgende Attribute enthalten:
id
username
password
email
role
Erstellen Sie dazu im Package htw.fiw.rest
ein neues Package user
und darin eine neue Java-Klasse User
. Diese Klasse sieht wie folgt aus:
Wir bräuchten nur die Annotation @Entity
, auf @Table
könnte verzichtet werden. Dann würde in PostgreSQL eine Tabelle erstellt werden, die user
heißt. Mit der @Table
-Annotation können wir z.B. deren Namen (auf users
) ändern.
Ein Java-Repository erstellen¶
Wir haben nun eine Entität definiert, für die eine Tabelle in der Datenbank erstellt wird. Für diese Tabelle wollen wir die CRUD-Funktionen implementieren, um
user
der Tabelle hinzuzufügen (**C**reate),user
auszulesen, einen, alle oder nach bestimmten Eigenschaften suchen (**R**ead),user
-Daten zu aktualisieren/zu ändern (**U**pdate),user
zu löschen (**D**elete).
Dafür existieren verschiedene Repository-Implementierungen, die diese CRUD-Funktionen bereits als abstrakte Methoden zur Verfügung stellen. Wir bleiben bei Hibernate (JPA) und verwenden das Interface JpaRepository
. Wir erstellen uns im package user
das Interface UserRepository
, welches von JpaRepository
erbt.
Damit haben wir bereits ein ausführbares Programm und sind nun in der Lage, CRUD-Funktionlitäten konkret zu implementieren. Zunächst wollen wir jedoch unsere Tabelle schonmal mit ein paar Daten vorbefüllen:
Daten laden¶
Wir erstellen uns eine Klasse LoadUserData
in unserem Package user
:
Spring Boot führt automatisch alle CommandLineRunner
-Beans aus, sobald die Anwendung. Der CommandLineRunner
in LoadUserData
erwartet ein UserRepository
und kann dafür save()
-Methode aufrufen, die 4 user
in die Tabelle user
einträgt.
pgAdmin4¶
Installieren Sie sich pgAdmin und verbinden Sie sich dort mit dem Ocean-Server. Beachten Sie, dass Sie bei der Verbindungsherstellung den Nutzernamen (und das Passwort) verwenden, mit dem Sie sich im Spring-Boot-Projekt authentifiziert haben. In pgAdmin
können Sie sich anschauen, ob die Daten in die Datenbank eingetragen wurden:

Sie können sich aber auch in IntelliJ direkt mit der Datenbank verbinden, siehe z.B. hier.
Passwörter verschlüsseln¶
An dieser Stelle fällt auf, dass wir die Passwörter unverschlüsselt in der Datenbank abspeichern. Das wollen wir natürlich nicht. Wir verwenden BCrypt zur Verschlüsselung und zum Vergleich von Passwörtern mit verschlüsselten Passwörtern. Dazu müssen wir zunächst folgende dependency unserem pom.xml
hinzufügen:
Nun fügen wir der (Programm-)Klasse RestApplication
ein Bean hinzu. Beans sind Komponenten in unserer Anwendung. Die run()
-Methode innerhalb der main()
-Methode liefert uns den sogenannten Anwendungskontext. Das sind, vereinfacht gesagt, alle Komponenten, die der Anwendung zur Verfügung stehen. Die Anzahl der Komponenten lässt sich erweitern. Wir erweitern um die Komponente BCryptPasswordEncoder
:
Dieses Bean lässt sich nun überall injizieren. Wir injizieren das Bean in unsere Klasse LoadUserData.java
und rufen die Methode encode()
auf, um unsere Passwörter zu verschlüsseln:
Die Passwörter werden nun verschlüsselt in der Datenbank abgelegt:
Endpunkte definieren - GET all¶
Nun müssen wir nur noch eine HTTP-Anbindung durchführen, d.h. unser Repository
wird von einer "Web-Schicht" umschlossen. Dazu verwenden wir die Annotation @RestController
in einer neuen Klasse UserController
. In diese Klasse injizieren wir das UserRepository
. Dann haben wir passend zu den HTTP-Anfragemethoden die entsprechenden Annotationen, um unsere Routen zu definieren:
Annotation für die Route | HTTP-Anfragemethode | CRUD-Funktion |
---|---|---|
@GetMapping |
GET |
R ead |
@PostMapping |
POST |
C reate |
@PutMapping |
PUT |
U pdate |
@DeleteMapping |
DELETE |
D elete |
Wir erstellen die Klasse UserController
und definieren uns unseren ersten Endpunkt GET /user
.
Wir können unseren Endpunkt nun bereits testen. Wir führen die Anwendung aus und der Webserver http://localhost
"lauscht" am Port 8080
auf eingehende Requests
. Da es sich um eine GET
-Anfrage handelt, können wir diese sogar im Browser aufrufen:
Für die anderen HTTP-Anfragemethoden können wir den Browser jedoch nicht mehr verwenden. Stattdessen sollten wir einen REST-Client, wie z.B. Postman installieren:
Eine weitere Möglichkeit besteht darin, HTTP-Request direkt in IntelliJ auszuführen, siehe dazu hier. Klicken Sie dazu links vom GET-Endpunkt
auf das kleine Icon Open in HTTP Client
. Es öffnet sich ein Editorfenster generated-request.htpp
. Dort können Sie den Endpunkt ausführen:
Wir verwenden im Folgenden Postman.
GET one by id¶
Wir wollen einen weiteren Endpunkt erstellen, GET /user/:id
, d.h. wir wollen den user
als Response erhalten, der in der Datenbank unter einer bestimmten id
gespeichert ist. Es handelt sich dabei um eine parametrisierte Route. Wir führen zwei Neuerungen ein:
- die Behandlung von parametrisierten Routen und
- die Behandlung von nicht gefundenen Ressourcen. Es kann ja sein, dass eine
id
angefragt wird, für die es keinen passenden Datensatz in der Datenbank gibt.
Zunächst der Endpunkt:
Wir übergeben der one()
-Methode die gesuchte id
als @PathVariable
. Es wird in der Datenbank nach einem user
mit der id
gesucht (findById()
) und dieser user
als Response zurückgegeben. Falls ein solcher user
(die id
) jedoch nicht existiert, werfen wir eine UserNotFoundException
. Diese definieren wir uns im Folgenden:
Die Klasse erbt von RunTimeException
und übergibt dem Konstruktor von RunTimeException
die error-Message "Could not find user with id=" + id
. Damit dieser 404
-Fehler auch in der Response erscheint (derzeit wird einfach nur eine Exception geworfen), benötigen wir noch eine @RestControllerAdvice
.
Der @ExceptionHandler
sorgt dafür, dass der @RestControllerAdvice
genau dann in das Response-Objekt eingefügt wird, wenn eine UserNotFoundException
geworfen wird. Der @ResponseStatus
wird auf die vordefinierte Konstatnte HttpStatus.NOT_Found
gesetzt, das ist der 404
-Http-Status.
Nach Neustart der Anwendung können wir diesen Endpunkt nun ausprobieren. Wenn die id
existiert (hier GET http://localhost:8080/user/2
), dann wird der entsprechende Datensatz zurückgegeben (Http-Status 200
):

Existiert der Datensatz jedoch nicht (hier GET http://localhost:8080/user/5
), dann erscheint eine Fehlermeldung mit Http-Status 404
:

POST new user¶
Wir erzeugen einen Endpunkt, mit dem wir einen neuen user
in der Datenbank anlegen können. Dazu wird als Http-Anfragemethode POST
verwendet, d.h. wir nutzen die Annotation @PostMapping
. Außerdem müssen nun die Daten über den neuen user
im Body
des Request
-Objektes mitgesendet werden. Dazu verwenden wir als Annotation des Parameters der Methode nun @RequestBody
. Eine sehr einfache Implementierung würde wie folgt aussehen:
Das würde auch bereits funktionieren:

In der oberen Hälfte in Postman wird das Request
-Objekt spezifiziert. Wir haben dort im Body
(beachte raw
und JSON
) den zu erzeugenden Datensatz definiert:
Außerdem wurde als Anfragemethode POST
gewählt und die Route ist /user
. Als Response
erhalten wir den neu erzeugten Datensatz in der Datenbank. PostgreSQL vergibt beim Einfügen automatisch eine eindeutige id
, diese wird uns in der Response
mitgeschickt.
Zwei Dinge stören jedoch an dieser einfachen Implementierung:
- das Password wird nicht verschlüsselt und
- es wird nicht geprüft, ob der
username
und/oder dieemail
eventuell bereits existier_t/en. Für den Fall soll der neueuser
nicht angelegt werden.
Wir kümmern uns zunächst um die Verschlüsselung des Passwortes. Wie bereits in der Klasse LoadUserData
injizieren wir das Bean BCryptPasswordEncoder
in die Klasse UserController
:
und können damit das Passwort verschlüsseln:

Um herauszubekommen, ob ein bestimmter username
oder eine email
bereits in der Datenbank existiert, bräuchten wir eine Methode findByUsername()
und/oder findByEmail()
. Wir haben beim GET /user/id
-Endpunkt bereits mit der findById()
-Methode gearbeitet. Es existiert im JpaRepository
jedoch weder die Methode findByUsername()
noch findByEmail()
. Diese müssen wir uns erst erstellen. Dazu ergänzen wir diese Methoden im UserRepository
:
Wir haben gleich drei Methoden hinzugefügt, um die unterschiedlichen Prinzipien zu zeigen.
- Das Erstaunliche ist, dass bereits die Methode
findByUsername(String username)
funktioniert, ohne dass eine@Query
dafür angegeben wurde. Spring erkennt an den Namen (der Methode und des Parameters), dass nachusername
gesucht wird. - Die Methode
List<User> findByEmail(String email)
würde deshalb auch ohne@Query
funktionieren, aber wir haben hier die Verwendung mal gezeigt. Für den namenlosen Parameter?1
wirdString email
eingesetzt. - Bei der Definition der Methode
findByUsernameOrEmail()
haben wir in der@Query
benannte Parameter verwendet (:username
und:email
). Das Binding an diese Parameter erfolgt mittels@Param()
.
Wir verwenden jetzt die findByUsernameOrEmail()
-Methode, um zu verhindern, dass ein user
neu hinzugefügt wird, dessen username
und/oder email
bereits in der Datenbank enthalten ist:
Wir werfen eine UserAlreadyExistsException
, falls der username
und/oder die email
bereits existiert. Diese UserAlreadyExistsException
-Klasse müssen wir uns erst noch erstellen:
und sie mit in die Klasse UserNotFoundAdvice
integrieren:
Ganz optimal ist der Name der Klasse nun nicht mehr, aber sobald eine UserAlreadyExistsException
wird der Http-Status 409
in der Response übermittelt und die Nachricht (Beispiel) user with username user5 and/or with email user5@test.de already exists
.
Wenn wir spezieller darauf reagieren wollen, ob genau der username
oder genau die email
bereits verwendet wird, müssen wir die Methode findByUsername()
und findByEmail()
verwenden. Derzeit benutzen wir diese gar nicht.
Eine weitere Verbesserungsmöglichkeit des Endpunktes besteht darin, dass die save()
-Methode den Http-Status 200
zurückgibt. Für das Erzeugen neuer daten gibt es jedoch extra den Http-Status 201 - Created
. Um diesen in der Response zurückzugeben, führen wir folgende Änderung durch:
Unsere Methode gibt nun eine ResponseEntity
typisiert mit User
zurück. Diese erzeugen wir mithilfe von new ResponseEntity<>()
, wobei der erste Parameter den Body unserer Response füllt und der zweite den Http-Status überträgt. Nun passt es:

PUT user¶
Wir erstellen uns einen Endpunkt, um die Daten eines bereits existierenden Nutzers zu ändern. Eine einfache Implementierung könnte wie folgt aussehen:
Es wird nach der id
(wird als Parameter der Route hinzugefügt) gesucht und wenn der Datensatz gefunden wird, werden die Eigenschaften (username
, email
und role
) mit den Daten aus dem Request-Body aktualisiert. Wird der user
jedoch nicht über seine id
gefunden, wird er der Datenbank neu hinzugefügt. Das beinhaltet jedoch mehrere Probleme:
- Der Endpunkt sollte genau nur zur Änderung eines bereits bestehenden
user
genutzt werden und nicht für ein Erzeugen eines neuen.orElseGet()
sollte also besser eineUserNotFoundException
werfen. - Die Daten aus dem Request-Body werden ungeprüft eingelesen. Dabei könnte es z.B. vorkommen, dass ein
user
auf einenusername
bzw. auf eineemail
gändert wird, die bereits existiert. Das wollten wir ja genau vermeiden (siehe oben Post new user). - Außerdem wäre es sinnvoll, dass sich der
user
mit seinem Passwort autorisiert, um überhaupt die Änderungen durchführen zu dürfen.
Die erste einfache Änderung ist das Werfen der UserNotFoundException
:
Nun prüfen wir, ob der neue username
bzw. die neue email
nicht bereits existiert:
Dabei ist zu beachten, dass es ja sein kann, dass der user
seinen username
und/oder seine email
behalten möchte und z.B. nur die Rolle ändern. Dann wäre ja die response-List
nicht leer, sondern würde den user
selbst enthalten. Wir prüfen deshalb neben response.isEmpty()
auch noch den Fall response.size() == 1 && response.getFirst().getId().equals(id)
.
Nun prüfen wir noch, ob das Passwort überhaupt korrekt ist. Das Passwort im Request-Body (newUser
) wird in Klarsicht übertragen (später per https
, deshalb ist es okay) und muss mit dem in der Datenbank verschlüsselt abgelegten Passwort übereinstimmen. Dafür beietet BCrypt die Methode matches(plain, hashed)
.
Falls das Passwort nicht übereinstimmt, werfen wir eine UserNotAuthorizedException
. Die Erstellung kennen wir bereits:
Angenommen, in unserer Datenbank existiert folgender user
:
{
"id": 5,
"username": "user5",
"password": "$2a$10$ngnQ0uPtXfc6QIihLHOjGu11DngHr3NesAjY7XKzUOFKC6cCgE7hi",
"email": "user5@test.de",
"role": "admin"
}
Das verschlüsselte Passwort entspricht pass1234
. Wir können nun folgende Anfragen stellen, um den Endpunkt zu testen:




DELETE one by id¶
Jetzt benötigen wir nur noch einen Endpunkt, um die CRUD-Funktionalitäten vollständig abgebildet zu haben: einen DELETE-Endpunkt, um einen Datensatz zu löschen. Wir löschen einen user
, indem wir seine id
als Parameter der Route hinzufügen:
REST-API
Wir haben unsere erste REST-API mit Spring Boot erstellt. Es handelt sich um eine einfache Nutzerverwaltung. Wesentliche Bestandteile sind
- die Entität
User
(KlasseUser
) zur Erstellung einer Tabelleusers
, - ein
UserRepository
(InterfaceUserRepository
) zur Bereitstellung der wesentlichen Datenbankmethoden (find(), save(), ...
), - ein
UserController
(KlasseUserController
) zur Bereitstellung der Endpunkte (HTTP-Anfragemethoden + Routen)
- eine Klasse
LoadUserData
, um bereits Daten bei Start der Anwendung in die Tabelle einzufügen, - mehrere Exception-Klassen (
UserNotFoundException
,UserAlreadyExistsException
,UserNotAuthorizedException
) und - eine Advice-Klasse (Klasse
UserNotFoundAdvice
), die dafür sorgt, dass bei Werfen einer Exception ein geeigneter Fehlercode als Response gesendet wird (und eine Fehlermeldung).