Schritt-für-Schritt Anleitung zum Aufbau einer Spring Boot Applikation für das Web
Die Todolisten-Webapp stellt eine Verwaltungsanwendung für Aufgaben da. Sie ist multitenant-fähig, d.h. kann von mehreren Personen gleichzeitig genutzt werden, ohne dass diese sich untereinander stören. Zusätzlich sind diverse Services eingebaut wie der Login über Google oder das Versenden von Emails über den Aufgabenstatus.
Die Applikation ist mithilfe von Spring Boot gebaut. Bei Spring Boot handelt es sich um ein Java-Framework, in welchem serverbasierte Anwendungen implementiert werden können. Dabei folgt es dem Prinzip Konvention vor Konfiguration (convention over configuration), indem die einzelnen Module des Frameworks mit einer Default-Konfiguration versehen worden sind und der Entwickler nur bei Änderungen eingreifen muss. Dies ist ein Unterschied zu dem Standard-Spring-Framework, wo der Entwickler selbst für die komplette Konfiguration zuständig ist.
Spring Boot lässt sich sehr einfach initialisieren, indem die Webseite Spring Initializr genutzt wird. Hier werden die Grundeinstellungen, welches Build-Tool, welche Programmiersprache in welcher Version, welche Spring-Boot-Version genutzt und welche Benennung das Projekt besitzen soll, angegeben. Zusätzlich lassen sich die einzelnen Module von Spring Boot wählen, die eingebunden werden sollen. Mit einem Ctrl-Enter lässt sich eine ZIP-Datei herunterladen, die sich in die Entwicklungsumgebung laden lässt. Dies ist der Grundaufbau des Projektes.
Grundsätzlich lässt sich eine Spring-Boot-Applikation auch lokal über die Kommandozeile mit spring init initalisieren. Einige Entwicklungsumgebungen geben ähnliche Hilfestellungen. Es ist auch möglich eine Spring-Boot-Applikation völlig per Hand aufzubauen.
Für den nächsten Abschnitt haben wir Java als Programmiersprache gewählt, Version 11 angegeben, Maven als Buildtool selektiert und die web-Unterstützung konfiguriert. Des Weiteren haben wir die Benennungsparameter des Projektes angegeben.
Spring Boot wird mit Hilfe von Maven oder Gradle gesteuert. Für Maven ist die zentrale Datei, die Datei pom.xml, in welcher die gelistet sind.
Erster wichtiger Teil dieser Datei ist die Referenz auf das Parent-Pom spring-boot-starter-parent. Hierdurch wird die Spring-Boot-Unterstützung aktiviert. Hier befindet sich auch die Versionsnummer von Spring Boot.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>SPRING_BOOT_VERSIONSNUMMER</version>
<relativePath/>
</parent>
Im Folgenden finden sich Parameter, die bei der Initialisierung angegeben worden sind:
<groupId>org.todolistwebapp</groupId>
<artifactId>todolistwebapp</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>todolistwebapp</name>
<description>Beispielprojekt einer einfachen Spring-Boot-Anwendung,
die eine Todo-Listen-Verwaltung darstellt</description>
<properties>
<java.version>11</java.version>
</properties>
Im nächsten Abschnitt werden die einzelnen Module von Spring-Boot eingebunden. Dieser Abschnitt kann beispielsweise wie folgt aussehen. Hier wird das Web-Modul eingebunden, welche die Zugriffe über HTTP(S) unterstützt.
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
Für das Testing befindet sich folgender Abschnitt in den Dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
Eine Referenz auf das Maven-Plugin für Spring-Boot darf nicht fehlen. Dadurch werden die Maven-Befehle für Spring-Boot verfügbar:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Die Startup-Datei einer Spring-Boot-Applikation, die auch in unserem Falle mitgeneriert wurde, sieht im einfachsten Falle wie folgt aus:
@SpringBootApplication
public class TodoListWebApplication {
public static void main(String[] args) {
SpringApplication.run(TodoListWebApplication.class, args);
}
}
Dazu kommt ein mitgenerierter Controller, der unter dem Pfad /helloworld
den unten angegebenen String ausgibt. Der Controller ist im Folgenden leicht angepasst:
@RestController
public class HelloWorldController {
@RequestMapping("/helloworld")
public String index() {
return "Hello World! Dies ist eine Beispiel-Spring-Boot Applikation";
}
}
Auch ein erster Test ist innerhalb der Applikation generiert worden. Er befindet sich im test
-Verzeichnis in demselben Package. Er wird wie folgt angepasst:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HelloWorldControllerIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testReturnsTheString() {
ResponseEntity<String> responseEntity = this.restTemplate.getForEntity("http://localhost:" + port + "/helloworld",
String.class);
assertEquals(200, responseEntity.getStatusCodeValue());
}
}
Der Test sorgt dafür, den in Spring eingebauten Server inklusive der Spring-Applikation hochzufahren. Dann wird der Server mit einem REST-Call betestet, ob das korrekte Ergebnis zurückgeliefert wird.
Der Parameter webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
sorgt dafür, dass ein freier Zufallsport für die Lokation des Servers genutzt wird. Dieser Port wird über die Annotation @LocalServerPort
in den Test injiziert. Die Annotation @Test
kennzeichnet den Test als Test.
Der Test lässt sich in der IDE (hier am Beispiel von Jetbrains IntelliJ gezeigt) mit einem Rechtsklick auf den Dateinamen HelloWorldControllerIntegrationTest
und dann Klick auf Run HelloWorldControllerIntegrationTest
starten. Alternativ können auch die grünen Pfeile an der Testklasse oder dem Test genutzt werden. In anderen IDEs geschieht das Starten des Tests ähnlich:
Um die Applikation zu starten, wird eine laufende Instanz von MariaDB oder MySql unter dessen Standardport 3306
benötigt. Hierfür kann entweder der Server installiert, gestartet und die Datenbank springbootdb
angelegt werden oder aber folgendes Docker-Kommando ausgeführt werden:
docker run -p3306:3306 -d -e MYSQL_DATABASE=springbootdb -e MYSQL_ROOT_PASSWORD=pass!word mariadb:latest
Die Applikation lässt sich direkt aus der IDE mit einem Rechts-Klick auf die Applikation und dann Run
starten:
Auch von der Kommandozeile lässt sich die Applikation starten. Hierzu muss zunächst die Applikation mit mvn package
gebaut worden sein. Danach befindet sich im target
-Ordner des Projektes eine JAR-Datei, die wie folgt ausgeführt werden kann.
cd target
java -jar todolistwebapp-0.0.1-SNAPSHOT.jar
Auch mit Maven kann die Applikation gestartet werden:
mvn spring-boot:run
Dies kann über das Maven-Menü auf der rechten Seite von IntelliJ geschehen. Hierzu wird zunächst das Maven-Menü aufgeklappt, dann das Maven-m in der Menüzeile angeklickt und zuletzt mvn spring-boot:run
angegeben und mit Enter quittiert:
Ist Docker lokal installiert, kann mithilfe von
docker run -p8080:8080 ctornau/todolistwebapp
das automatisch generierte Docker-Image heruntergeladen und gestartet werden. Dabei handelt es sich nicht anders als bei den anderen Startmöglichkeiten um einen Build, der auf der lokalen Version basiert, sondern der auf der im Repository eingecheckten.
Weiter unten ist beschrieben, wie die Applikation innerhalb eines Kubernetes-Clusters betrieben werden kann.
Es ist darauf zu achten, dass in jedem Fall ein MySQL-Server bzw. MariaDB-Server lokal installiert und gestartet ist. Weiterhin ist MongoDB in einer Default-Konfiguration nötig.
In jedem Fall ist die Applikation unter http://localhost:8080/helloworld verfügbar.
Bei Thymeleaf handelt es sich um eine Templatesprache, welche es ermöglicht, HTML-Seiten mit Werten aus Modell-Objekten zu befüllen. Thymeleaf muss zunächst in die Dependencies eingebunden werden:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
Innerhalb des Verzeichnisses src/main/resources
wird ein Template-Verzeichnis templates
angelegt. In dieses Template-Verzeichnis werden Template-Dateien für Thymeleaf hinterlegt.
Innerhalb des Verzeichnisses templates
befindet sich die Datei index.html
. Diese beinhaltet Thymeleaf-Tags:
<!doctype html>
<html lang="de" xmlns:th="http://www.thymeleaf.org">
<head>
...
<link rel="stylesheet" th:href="@{/bootstrap/css/bootstrap.min.css}">
</head>
<body>
<table>
...
<tr th:each="todotask: ${tasks}">
<td th:text="${todotask.name}">Todotask Name</td>
<td th:text="${#calendars.format(todotask.creationTime,'dd. MMMM yyyy')}"></td>
<td th:text="${#calendars.format(todotask.startTime,'dd. MMMM yyyy')}"></td>
<td th:text="${#calendars.format(todotask.finishTime,'dd. MMMM yyyy')}"></td>
<td th:text="${todotask.state.displayValue}"></td>
</tr>
...
</table>
</body>
Das Framework Bootstrap ist ein Aufsatz auf die Darstellung von HTML und CSS innerhalb eines Browsers. Es sorgt für ein uniformes Bild über verschiedene aktuelle Browser hinweg und bietet Erweiterungen zur Sprache an, so dass sich Webseiten relativ schnell zusammenstellen lassen. Innerhalb der HTML-Dateien index.html
und weiteren wird Bootstrap genutzt.
Weiterhin kommt die Erweiterung DataTables zum Einsatz. Diese ermöglicht es HTML-Tabellen interaktiv im Browser darzustellen. Durch DataTables erscheint im finalen Programm eine Tabelle, die sich sortieren und durchblättern lässt.
Innerhalb von Spring Boot lässt sich mithilfe von Spring Data ein Zugang zu einer Datenbank realisieren.
Um Spring Data zu nutzen, muss es via Dependency in der pom.xml
eingebunden werden:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>
Die Spring Data JPA stellt dabei ein sogenanntes Repository zur Verfügung, welches als DAO (Data Access Object) fungiert. Dabei muss die Klasse nicht selbst geschrieben werden, sondern sie wird von Spring Boot Data JPA selbständig aus dem Interface generiert:
package org.todolistwebapp.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.todolistwebapp.model.TodoTask;
import java.util.List;
@Repository
public interface TodoTaskRepository extends JpaRepository<TodoTask, Long> {
List<TodoTask> findByOwner(String owner);
}
Das JpaRepository stellt die unterschiedlichsten Möglichkeiten bereit, auf die Daten der Tabelle zuzugreifen. Für uns wichtig ist die findAll()
-Methode, welche alle Datensätze liefert, wie die getOne(long id)
-Methode, welche anhand der ID einen Datensatz aus der Datenbank liefert.
Im Hintergrund ist das Beispielprojekt so konfiguriert, dass auf eine Datenbank MariaDB zugegriffen wird, die lokal zu installieren ist, um die Applikation zu starten.
Der Treiber für die Datenbank muss über eine Dependency in der pom.xml
eingebunden werden:
<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.6.0</version>
</dependency>
</dependencies>
Für die Tests ist eine andere Dependency konfiguriert, damit ohne das Hochfahren von MariaDB getestet werden kann:
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
<scope>test</scope>
</dependency>
</dependencies>
Innerhalb der application.properties
befinden sich die Konfiguration von Spring Data wie auch die Credentials der Datenbank:
spring.datasource.url=jdbc:mariadb://localhost:3306/springbootdb
spring.datasource.username=root
spring.datasource.password=
spring.jpa.generate-ddl=true
Die Datenbank springbootdb
muss manuell hinzugefügt werden, damit Spring Boot hier die entsprechenden Daten ablegen kann.
Die meisten Applikationen erfordern ein Login: Nicht jeder darf auf alle Daten der Applikation zugreifen. Auch hier hat Spring Boot eine Möglichkeit, Zugriffe zu konfigurieren: Spring Boot Security.
Wir nutzen Spring Boot Security, um eine OAuth2-Authentifizierung durchzuführen. Dabei wird der Benutzer nicht lokal gegen ein Passwort authentifiziert, sondern von einem OAuth2-Provider wie bspw. Google.
Spring Boot Security wird wie folgt in der pom.xml
eingebunden:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Innerhalb der pom.xml
sind noch weitere Libraries eingebunden, die noch nicht von Spring Boot Security gegriffen werden, aber für eine OAuth2-Authentifzierung benötigt werden.
Die Datei SecurityConfig.java
übernimmt die Konfiguration der Spring Boot Security. Zusätzlich werden in den application.properties
Schnlüssel angelegt, die über die Credentials Console in diesem Fall von Google bezogen werden können:
spring.security.oauth2.client.registration.google.clientId=die-client-id.apps.googleusercontent.com
spring.security.oauth2.client.registration.google.clientSecret=das-secret
Diese Werte werden durch die Kubernetes-Environment-Variablen, sollte ein Kubernetes-Deployment stattfinden, überschrieben (in k8s/springboot/deployment.yaml
):
- name: spring.security.oauth2.client.registration.google.clientId
value: die-client-id.apps.googleusercontent.com
- name: spring.security.oauth2.client.registration.google.clientSecret
value: das-secret
Der Dienst SendGrid ist ein Cloud-Dienst zum Versenden von Emailnachrichten. Theoretisch wäre es möglich, von jedem beliebigen Rechner Emailnachrichten zu versenden. Jedoch schützen sich die meisten Empfängerserver mittlerweile vor Emails von zuvor unbekannten IP-Adressen und führen auch weitere Schutzmaßnahmen durch, um Spam zu unterdrücken. Deshalb nutzt man am besten einen entsprechenden Dienst, der den Versand der Emails managt.
Innerhalb der pom.xml
ist die Bibliothek von SendGrid wie folgt eingebunden:
<dependencies>
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>
Innerhalb der application.properties
müssen der dort zur Verfügung gestellte API-Key eingetragen werden. Ebenso muss über SendGrid die Absenderadresse verifiziert werden und eingetragen werden:
sendgrid.apikey=der-Key
sendgrid.senderemailaddress=die-absenderadresse@somewhere.com
Auch diese Werte werden bei einem Kubernetes-Deployment durch die Environment-Variablen (in k8s/springboot/deployment.yaml
) überschrieben:
- name: sendgrid.apikey
value: key-von-sendgrid
- name: sendgrid.senderemailaddress
value: verifizierte-emailadresse-bei-sendgrid
Im Folgenden wird nun ein Kubernetes-Deployment der Applikation auf einem beliebigen Kubernetes-Cluster aufgebaut.
Kubernetes- Deployment von MariaDB mithilfe von
kubectl run mariadb --image=mariadb:latest --env="MYSQL_ROOT_PASSWORD=pass!word" --env="MYSQL_DATABASE=springbootdb" --port=3306
Das Command kubectl get pods
zeigt einen Pod:
NAME READY STATUS RESTARTS AGE
mariadb-67fb996878-q5qmm 1/1 Running 0 2m28s
Mithilfe von kubectl delete pod mariadb-***
lässt sich der Pod entfernen. Dies wird von Kubernetes erkannt und als ausgleich sogleich ein neuer Pod gestartet. Dies kann mit kubectl get pods
geprüft werden, wo sich der Pod-Name und die Zeit nun verändert haben sollten.
Mithilfe von
kubectl get deployment mariadb -o yaml > k8s/mariadb/deployment.yaml
lässt sich das Deployment in einer YAML-Datei persistieren (Das Verzeichnis k8s/mariadb
muss dazu existieren).
Das Deployment kann mit kubectl delete deployment mariadb
wieder entfernt werden.
Wir editieren die Datei, so wie sie im Git-Repository schon hinterlegt ist. Mit dem Befehl kubectl apply -f k8s/mariadb/deployment.yaml
lässt sich das Deployment aus der editierten YAML-Datei (im Git-Repository schon hinterlegt) wieder einspielen. Ein neuer Pod wird wieder gestartet. Dies kann mit kubectl get pods
geprüft werden.
Mithilfe eines Services kann auf MariaDB zugegriffen werden. Wir deployen einen Service mit Cluster-IP, der dafür sorgt, dass Kubernetes-Intern MariaDB aufgerufen werden kann. Der Service ist in der Datei service.yaml
hinterlegt:
kubectl apply -f k8s/mariadb/service.yaml
Mit kubectl get service
lassen sich installierte Services im Kubernetes-Cluster anzeigen:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 3d7h
mariadb ClusterIP None <none> 3306/TCP 10m
Nun starten wir ein weiteres Deployment, welches über den MariaDB-Client auf der Kommandozeile auf den MariaDB-Server zugreift:
kubectl run -it --rm --image=mariadb:latest --restart=Never mariadb-client -- mariadb -h mariadb -ppass!word
Mit show databases;
lässt sich die Liste der Datenbanken anzeigen und wir erkennen, dass die Datenbank springbootdb
angelegt worden ist:
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| springbootdb |
+--------------------+
4 rows in set (0.000 sec)
Das Deployment der Datenbank MongoDB verhält sich ähnlich.
Mit dem Kommando
kubectl apply -f k8s/mongodb/deployment.yaml
wird der Pod deployt.
Dieser Pod erhält einen Service mit:
kubectl apply -f k8s/mongodb/service.yaml
Möchte man seine eigenen Änderungen testen, so ist das Docker-Image wie folgt zu bauen:
docker build . -t todolistwebapp:1
Bei eigenen Änderungen muss das referenzierte Docker-Image in k8s/springboot/deployment.yaml
angepasst werden. Hier ist das öffentliche Image des Builds standardmäßig angegeben. Dann kann ein Deployment wie folgt erfolgen:
kubectl apply -f k8s/springboot/deployment.yaml
Das Deployment kann verifiziert werden:
kubectl get pods
Es sollte in etwa diese Ausgabe zu sehen sein:
NAME READY STATUS RESTARTS AGE
mariadb-7999774f68-ml2sl 1/1 Running 0 6h26m
mongodb-665d847848-sd42p 1/1 Running 0 100m
todolistwebapp-5c7c8d49f5-kzr4w 1/1 Running 0 56m
Die Pod-internen Logs lassen sich wie folgt anzeigen:
kubectl logs todolistwebapp-5c7c8d49f5-kzr4w
Man erkennt an der folgenden Ausgabe Started TodoListWebApplication in 4.415 seconds
, dass der Start erfolgreich war:
2020-08-15 16:23:22.665 INFO 1 --- [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page template: index
2020-08-15 16:23:22.675 INFO 1 --- [ task-1] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2020-08-15 16:23:22.688 INFO 1 --- [ task-1] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2020-08-15 16:23:22.847 INFO 1 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2020-08-15 16:23:22.850 INFO 1 --- [ main] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
2020-08-15 16:23:23.014 INFO 1 --- [ main] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
2020-08-15 16:23:23.025 INFO 1 --- [ main] o.todolistwebapp.TodoListWebApplication : Started TodoListWebApplication in 4.415 seconds (JVM running for 5.313)
Unsere Spring-Boot-Applikation soll von extern erreicht werden können. Kubernetes bietet mehrere Möglichkeiten, dies zu bewerkstelligen. Wir wählen die Möglichkeit des Ingress. Hier befindet sich innerhalb von Produktionsumgebungen LoadBalancer vor der Applikation, die den Traffic aufteilen.
Doch zunächst muss ein Service erstellt werden:
kubectl apply -f k8s/springboot/service.yaml
Dann erzeugen wir mit dem folgenden Befehl einen Nginx Ingress. Nginx ist ein Webserver, der sich auch als Reverse-Proxy benutzen lässt:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud/deploy.yaml
Mit Hilfe des folgenden Befehls lässt sich anzeigen, ob der Ingress korrekt eingerichtet worden ist:
kubectl -n ingress-nginx get pod,service
Die Ausgabe sollte in etwa wie folgt aussehen:
NAME READY STATUS RESTARTS AGE
pod/ingress-nginx-admission-create-2bh27 0/1 Completed 0 23s
pod/ingress-nginx-admission-patch-xzvcp 0/1 Completed 1 23s
pod/ingress-nginx-controller-77f5884bdd-mfqw4 1/1 Running 0 23s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/ingress-nginx-controller LoadBalancer 10.96.255.145 localhost 80:31110/TCP,443:31396/TCP 23s
service/ingress-nginx-controller-admission ClusterIP 10.98.124.103 <none> 443/TCP 23s
Nun muss dem Nginx-Server mitgeteilt werden, unter welcher URL er die hereinkommenden Requests routen soll. Dies wird wie folgt getan:
kubectl apply -f k8s/springboot/ingress.yaml
Nun sollte die Application unter der URL http://localhost:80 lokal erreichbar sein. Ggf. muss OAuth2 zusätzlich konfiguriert werden, dass auch von der neuen URL Requests entgegengenommen werden.