Model-View-Controller mit Spring und Thymeleaf

Thymeleaf ist eine moderne Template Engine, um Server-seitig html zu generieren. Hier zeige ich, wie es in einem Spring MVC Projekt eingesetzt wird. Dabei demonstriere ich verschiedene Thymeleaf Ausdrücke (if, loop, usw.), Lesen des Modells und Nutzung von CSS oder JavaScript.


Model-View-Controller mit Spring und Thymeleaf

Betrachten wir das bekannte Design Pattern MVC (siehe auch rest-json-apis-in-java-leicht-gemacht), dann können wir die einzelnen Bestandteile mit dem hier vorgestellten Technologie-Stack implementieren:
  • Das Modell (Model) wird mit einfachen Java Objekten realisiert (POJO).
  • Die Ansicht (View) wird mittels Thymeleaf in html, css und JavaScript implementiert.
  • Der Controller wird als Spring Bean vom Typ @Controller umgesetzt.

Thymeleaf (https://www.thymeleaf.org/) ist eine Template Engine deren Templates in html geschrieben werden. Dynamische Stellen im html werden dann durch Thymeleaf Ausdrücke definiert. Z.B. blenden wir mit folgendem if-Ausdruck eine Nachricht (bzw. das ganze <p>-Tag) ein oder aus:
    <p th:if="${#lists.isEmpty(items)}">Die Liste ist leer.</p>

itmes ist eine Liste von Item Objekten im Modell, das vom Controller an die Thymeleaf View übergeben wurde. th: ist der XML Namensraum (Namespace) von Thymeleaf, der weiter oben im selben Dokument definiert sein muss.
Variable Ausdrücke, die Thymeleaf zur html Generierung auswertet, werden unter anderem mit ${...} markiert. #lists ist ein von Thymeleaf bereitgestelltes Hilfs-Objekt für Listen-Operationen, wir nutzen hier die isEmpty Methode, um das <p>-Tag nur dann einzublenden, wenn die Liste items leer ist. 

YouTube Video zum Artikel

Thymeleaf ins Java Projekt integrieren

Thymeleaf befindet sich als Java-Bibliothek im Maven Repository und kann somit als Dependency in jedes Maven oder Gradle Projekt geladen werden. Ein bestehendes Spring Boot Projekt (siehe microservices-mit-spring-boot-erstellen) kann durch Hinzufügen der von Spring Boot gemanagten Thymeleaf Dependency ergänzt werden:
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

Spring Controller

Der Controller für die Thymeleaf View wird in Spring analog zu einem RestController erstellt (siehe auch rest-json-apis-in-java-leicht-gemacht). Der wesentliche Unterschied ist, dass wir in jeder Controller-Methode, die nächste anzuzeigende View festlegen müssen.

Unsere Demo Web-Anwendung soll die folgenden UseCases umsetzen:
  • Liste mit allen erstellten Dingen (Items) anzeigen.
  • Neues Ding erstellen und der Liste hinzufügen.
Der Spring MVC Controller für diese Anwendung sieht so aus:

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class ItemController {

    private List<Item> items = new ArrayList<>();
    private static final String MODEL_KEY_ITEMS = "items";
    private static final String VIEW_NAME_LIST = "item-list";
    private static final String VIEW_NAME_CREATE = "item-create";
    @GetMapping(path = {"/", "/item"})
    public ModelAndView showItems(ModelAndView mav) {
     mav.setViewName(VIEW_NAME_LIST);
mav.addObject(MODEL_KEY_ITEMS, items);
return mav;
    }
    @GetMapping("/item-create")
    public String showCreateItem() {
     return VIEW_NAME_CREATE;
    }
    @PostMapping("/item") 
    public String createNewItem(Model model,
    @RequestParam String itemname, 
            @RequestParam String itemid) {
var item = new Item();
item.setName(itemname);
item.setId(itemid);
items.add(item);
model.addAttribute(MODEL_KEY_ITEMS, items);
return VIEW_NAME_LIST;
    }
}

  • Den klassischen MVC Controller annotieren wir in Spring mit @Controller. Dadurch wird der ItemContoller als Bean im Spring IoC Container bekannt. Eingehende Requests werden vom Dispatcher Servlet entsprechend dem registrierten Handler Mapping an den richtigen Controller geschickt.
  • @GetMapping und @PostMapping kennen wir schon aus diesem Artikel rest-json-apis-in-java-leicht-gemacht. Das Spring Handler Mapping basiert auf den hier konfigurierten Pfaden, so dass eingehendet Request entsprechend ihres Typs und URL-Pfades an die jeweilige Methode des Controllers geschickt werden.
    Die Methode showItems bearbeitet hier eingehende GET Requests für 2 verschiedene Pfade - generell können beliebig viele Pfade bzw. eine Liste von Pfaden auf eine Methode gemappt werden.
  • @RequestParam mappt die Daten innerhalb des POST Formulars auf den annotierten Parameter in der Methode createNewItem. Das Formular stelle ich in einem zweiten Artikel zu Thymeleaf html Templates vor. 
  • Model : Die Methode createNewItem hat den Parameter model, der automatisch eine Instanz des Model Interface bereitstellt. Diese Model Instanz kann dann mit den Daten des Modells befüllt werden und in der View ausgelesen werden (siehe Kapitel weiter unten). Model ist vergleichbar mit einer Map, daher kann eine Model Instanz auch mit der Methode asMap als Map Instanz verarbeitet werden. 
  • ModelAndView : Die Methode showItems hat den Parameter mav vom Typ ModelAndView. Die ModelAndView Klasse ist ein Kombination aus dem Model und der anzuzeigenden View. Mit mav.addObject werden Objekte dem Modell innerhalb der Klasse ModelAndView hinzugefügt. Mit setView oder setViewName wird die anzuzeigende View definiert. Im Beispiel hier setze ich immer den String viewName, dessen Wert dem Namen der Thymeleaf html-Templates mit oder ohne Dateiendung ".html" entsprechen muss. 
  • Wenn die Methode einfach nur einen String zurückgibt (siehe showCreateItem), versucht Spring eine passende View zu finden und diese anzuzeigen. Findet Spring keine View, kommt es zu einem "Internal Server Error".
    Hier unterscheiden sich
    @RestController vom @Controller
    Der RestController interpretiert den Sting als den Inhalt der Response und nicht als die nächste anzuzeigende View. Gibt man im RestController ein View Objekt zurück, so zeigt auch dieser die View an.
    Ihr könnt das einfach testen, indem ihr in unserem Demo Projekt einfach mal die ItemController Annotation ändert.

POJO Modell Klasse

Die Modell Klassen sind einfache POJO (Plain Old Java Object) Klasse. Das Besondere daran ist, dass es keine Besonderheiten gibt. 😄
Daher zeige ich euch hier einfach kurz die Item Klasse bestehend aus Attributen, Getter-, Setter-Methoden und sonst nichts.

    public class Item {
private String id, name;

public String getId() { return id; }
public void setId(String id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }
    }

Thymeleaf View

Nachdem wir in den vorherigen Kapiteln über den Controller und das Modell gelesen haben, kommen wir nun zur Ansicht (View), die mit Thymeleaf umgesetzt wird.

Controller und Modell könnten wir auch mit beliebigen anderen View Technologien kombinieren, wie zum Beispiel:
Es gibt noch viele mehr, hier möchte ich mich aber ausschließlich auf Thymeleaf konzentrieren.

HTML, CSS und JavaScript Dateien richtig ablegen

In unserem Spring Boot Thymeleaf Projekt gibt es Standard-Verzeichnisse für HTML-, CSS- und JavaScript-Dateien. Diese nutzen wir hier auch, um das Tutorial einfach zu halten.

Im src/main/resources Verzeichnis, wo sich auch die Spring application.properties Datei befindet, erstellen wir 2 Unterordner: static und templates.
  • templates ist der Standard Ordner für alle Thymeleaf Templates, die in Form von html Dateien gespeichert werden.
  • static wird für statische Dateien, wie JavaScript- und CSS-Dateien verwendet. Es empfiehlt sich mit weiteren Unterverzeichnissen eine Ordnung zu schaffen. Ich habe daher hier die beiden Unterverzeichnisse js für JavaScript-Dateien und css für Cascading Style Sheets angelegt.
Die JavaScript und CSS Dateien sind unabhängig von Thymeleaf und sehen mit anderen View-Technologien genau so aus. Daher gehe ich hier nicht weiter auf diese Dateien ein - ihr könnt sie aber gerne in meinen GitHub Projekt zusammen mit dem anderen Code genauer anschauen:
https://github.com/elmar-brauch/thymeleaf

Verzeichnis-Struktur Thymeleaf

Thymeleaf Templates

Normale html-Dateien könnten auch von Thymeleaf ausgespielt werden, so dass man die html-Dateien auch Schritt für Schritt in Thymeleaf Templates umwandeln kann. In meinem GitHub Projekt findet ihr Thymeleaf Templates, die mehr Funktionen nutzen. Hier im Blog Teil 1 möchte ich es aber auf wenige Elemente beschränken. Im Teil 2, einem künftigen Post, zeige ich dann weitere Thymleaf Funktionen.
Also hier die vereinfachte Template Datei "src/main/resources/templates/item-list.html":

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,
                initial-scale=1, shrink-to-fit=no" />
    <title>Spring Boot Thymeleaf Demonstration</title>
    <link rel="stylesheet" th:href="@{/css/main.css}" />
    <script type="text/javascript" th:src="@{/js/main.js}"></script>
</head>
<body>
    <main role="main" class="container">
    <div class="starter-template">
<h1>Ding Sammlung</h1>
<p th:if="${#lists.isEmpty(items)}">
    Ding Sammlung ist leer...
</p>
<ol th:unless="${#lists.isEmpty(items)}">
   <li th:each="item : ${items}" th:text="${item.name}"></li>
</ol>
<h2><a th:href="@{/item-create}">Erstelle Ding</a></h2>
    </div>
    </main>
</body>
</html>

Für Infos zu html verweise ich auf https://wiki.selfhtml.org/wiki/HTML.
Die Thymeleaf Ausdrücke erkennt man hier am Prefix "
th:":
  • xmlns:th="http://www.thymeleaf.org" ist die Definition des XML Namespaces für Thymeleaf. Das wird benötigt, damit die folgenden Thymeleaf Ausdrücke als solche erkannt werden.
  • Mit th:href="@{/css/main.css}" wird hier eine Hyper-Referenz (href) auf die verwendete CSS Datei main.css gemacht. Der Pfad /css/main.css muss sich im Spring resource Folder befinden. Links zu URLs oder Dateien werden immer mit @{...} definiert.
  • th:src="@{/js/main.js}" referenziert hier die JavaScript Quellen-Datei (src) "/js/main.js". Es funktioniert analog zur th:href Hyper-Referenz.
  • <a th:href="@{/item-create}"> : th:href kann auch für Links (in a-Tags) zu anderen Views genutzt werden, die dann wieder über den Spring Controller ausgespielt werden, siehe ItemController Klasse showCreateItem Methode.
  • th:if="${#lists.isEmpty(items)} ist ein Thymeleaf if-Ausdruck zum Ein- oder Ausblenden des zugehörigen Tags. Die Erklärung dieses Ausdrucks findet ihr bereits im ersten Abschnitt dieses Blog-Posts.
    Hier möchte ich noch mal darauf eingehen, wo die
    items Liste her kommt. Schauen wir uns noch mal unseren Controller an. Dort sehen wir, dass eine Liste mit Item Objekten zum Modell per Methode addAttribute hinzugefügt wurde. addAttribute verwendet als Schlüssel den Wert "items", daher können wir in Thymeleaf mit items auf den Wert im Modell, also die List<Item> Instanz zugreifen. 
  • th:unless="${#lists.isEmpty(items)} bzw. th:unless ist die negierte Form vom vorherigen th:if. Es entspricht also exakt diesen Ausdruck: 
    th:if="${not #lists.isEmpty(items)}
  • th:each="item : ${items}" ist eine for-each Schleife, wie wir sie auch aus Java kennen. ${items} greift auf die List<Item> Instanz in unserem Modell zu. Mit th:each iteriert Thymeleaf über alle Element in der Liste. Innerhalb der Tags (im Beispiel <li>) im Schleifen Tag (im Beispiel <ol>) können wir mit ${item} auf die einzelnen Listenelemente zugreifen. 
  • th:text="${item.name}" wird verwendet um einen Text in einem Tag auszuspielen. Der Text wird aus der Variablen item bzw. deren Attribute name ausgelesen.
Im Browser sieht die Seite dann so aus, wenn man http://localhost:8080 aufruft und ja, das css für die Seite gewinnt keinen Schönheitswettbewerb 🙈

Fazit & Ausblick

In diesem Blog-Post haben wir gesehen, wie man mit Spring MVC Web-Applikationen bauen kann. Die html Seiten haben wir mit Thymeleaf Templates erstellt. Variable Elemente wurden vom Controller ins Modell geschrieben und im html-Template mit Thymeleaf Ausdrücken ausgelesen.

Im 2. Teil zu Thymeleaf stelle ich folgende Aspekte vor:

  • Schreiben ins Modell bzw. html Formulare
  • JavaScript in Thymeleaf Templates
  • Iternationalisierung (i18n) bzw. mehrere Sprachen mit Spring
  • Wiederverwendung von Thymeleaf Templates - z.B. dasselbe html head-Tag in mehreren Seiten anzeigen
Den kompletten Code inklusive dem Code für Teil 2 findet ihr hier in GitHub:
https://github.com/elmar-brauch/thymeleaf

Weiterführende Informationen zu Thymeleaf findet ihr hier:
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html



Kommentare

Beliebte Posts aus diesem Blog

Authentifizierung in Web-Anwendungen mit Spring Security 6

OpenID Connect mit Spring Boot 3

Erfahrungsbericht Spring Boot 3 Update