Im Java Ökosystem haben wir bereits die Wahl zwischen unterschiedlichen Templating Engines von beispielsweise JSP über Thymeleaf bis zu FreeMarker. Wieso also eine weitere Option dazuholen? Wir schauen uns die Vorteile von Qute in einem kleinen Beispielprojekt an um genau das herauszufinden.
Die Bibliothek Qute ist spezifisch für die Anforderungen von Quarkus entwickelt worden und kann dort neben dem Templating für Webseiten auch z.B. in der Mailer-Erweiterung oder andere Templating Aufgaben verwendet werden. Dabei führt Qute möglichst viele Schritte bereits zur Build Zeit durch um zur Laufzeit Ladezeiten zu verringern und Fehler frühzeitig zu erkennen. Damit eignet sich Qute auch gut für nativ kompilierte Anwendungen. Die Validierung und Verarbeitung der Templates zur Build Zeit deckt Syntax- und Typfehler zuverlässig und frühzeitig auf und verringert die Zeit für manuelles Testen und Debuggen. Die Bibliothek unterstützt wahlweise ein reaktives Paradigma und bietet eine sehr gute Integration in den Quarkus Dev-Modus. Über die Quarkus IDE Tools werden Probleme auch bereits beim Entwickeln markiert und Vorschläge werden für bekannte Typen unterstützt.
Für unsere Beispielanwendung erstellen wir eine kleine Verwaltung der Planeten unseres Sonnensystems. Die Web-Schnittstelle realisieren wir mit Jakarta REST, dafür können wir folgende Maven-Dependency verwenden.
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-qute</artifactId>
</dependency>
Qute Templates erstellen
Zunächst brauchen wir dann ein Template für eine Übersichtsseite. Templates werden für die quarkus-rest-qute
Integration überlicherweise im Verzeichnis src/main/resources/templates
und möglichen Unterverzeichnissen angelegt. Für die typensichere Integration ist standardmäßig vorgesehen, dass Templates die in der Klasse PlanetResource
verwendet werden in einem Unterordner mit demselben Namen liegen. Qute-Ausdrücke werden durch geschweifte Klammern eingeleitet und geschlossen. Eine Dokumentation der verschiedenen Möglichkeiten gibt es hier. In unserem Beispiel verwenden wir diese Arten von Ausdrücken.
- Einfache Ausdrücke:
{planet.name}
- Ternäre Ausdrücke:
{planet.hasRings ? 'Ja' : 'Nein'}
- Schleifen:
{#for planet in planets}
und{/for}
- Bedingte Bereiche:
{#if planets.isEmpty()}
und{/if}
- Direkter Zugriff auf benannte CDI-Beans:
{cdi:vertxRequest.getParam('search', '')}
- Automatisches Einbinden von CSS/JS Resourcen mit
quarkus-web-bundler
:{#bundle /}
Hier sind die entsprechenden Zeilen auch noch einmal im Kontext markiert.
<!DOCTYPE html>
<html lang="de">
<head>
[...]
{#bundle /}
[...]
</head>
<body>
[...]
<form action="/planets/ui" method="get">
<div class="input-group">
<input type="text" name="search" class="form-control" placeholder="Suche Planeten..."
value="{cdi:vertxRequest.getParam('search', '')}">
<button class="btn btn-outline-secondary" type="submit">Suche</button>
</div>
</form>
[...]
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>Name</th>
<th>Durchmesser (km)</th>
<th>Abstand zur Sonne (Mio. km)</th>
<th>Hat Ringe</th>
<th>Anzahl der Monde</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{#for planet in planets}
<tr class="planet-row">
<td><a href="/planets/ui/{planet.name}">{planet.name}</a></td>
<td>{planet.diameterKm}</td>
<td>{planet.distanceFromSunMillionKm}</td>
<td>{planet.hasRings ? 'Ja' : 'Nein'}</td>
<td>{planet.numberOfMoons}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/planets/ui/{planet.name}" class="btn btn-info">Anzeigen</a>
<a href="/planets/ui/{planet.name}/edit" class="btn btn-warning">Bearbeiten</a>
<form action="/planets/ui/{planet.name}/delete" method="post">
<button type="submit" class="btn btn-danger btn-sm"
style="border-top-left-radius: 0; border-bottom-left-radius: 0;"
onclick="return confirm('Möchten Sie {planet.name} wirklich löschen?')">
Löschen
</button>
</form>
</div>
</td>
</tr>
{/for}
{#if planets.isEmpty()}
<tr>
<td colspan="6" class="text-center">
Keine Planeten gefunden.
<a href="/planets/ui/new">Fügen Sie einen Planeten hinzu</a>.
</td>
</tr>
{/if}
</tbody>
</table>
[...]
</body>
</html>
src/main/resources/templates/PlanetResource/planets.html
Qute Templates verwenden
Sobald wir das Template fertiggestellt haben können wir uns dem Rendern der Seite zuwenden. Die Jakarta REST Methoden müssen dafür ein Objekt vom Typ TemplateInstance
zurückgeben. Um Templates typensicher zu definieren gibt es mit Qute zwei Möglichkeiten. Die erste Möglichkeit ist es die Templates über public static native
Methoden innerhalb einer statischen Klasse mit der Annotation @CheckedTemplate
zu referenzieren. Die Methode planets
referenziert standardmäßig dann die Template Datei src/main/resources/templates/PlanetResource/planets.html
. In den Methoden Parametern können die für das Rendern notwendigen Daten mit dem Parameternamen als Key definiert werden. Über diesen Namen können wir diese Daten im Template verwenden. Aufgrund des native
Keywords müssen wir keine Implementierung vornehmen und können die Methode einfach verwenden um unsere TemplateInstance
zu erstellen. Die Implementierung übernimmt Qute für uns.
@Path("/planets/ui")
public class PlanetResource {
@Inject
PlanetService planetService;
@CheckedTemplate
static class Templates {
public static native TemplateInstance planets(List<Planet> planets);
public static native TemplateInstance planet(Planet planet);
public static native TemplateInstance error(Integer code, String title);
}
@GET
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getPlanetsPage(@QueryParam("search") String search) {
List<Planet> planetList = search != null && !search.trim().isEmpty()
? planetService.searchPlanets(search)
: planetService.getAllPlanets();
return Templates.planets(planetList);
}
[...]
}
src/main/java/de/gedoplan/showcase/web/PlanetResource.java
Probleme mit dem Schlüsselnamen oder einem Methodennamen fallen bereits zur Compilezeit auf und der Vorgang wird abgebrochen. Läuft die Anwendung zu diesem Zeitpunkt bereits im Dev-Modus wird nach einem Reload im Dev-Modus direkt auf die Stelle im Template verwiesen, die für den Fehler verantwortlich ist.
Eine andere Möglichkeit Templates zu referenzieren funktioniert über Records. Für das Template PlanetForm.html
definieren wir einen Record PlanetForm
um neue Planeten anzulegen oder bestehende zu bearbeiten. Der Record muss das Interface TemplateInstance
implementieren um Qute zu signalisieren, dass es sich hierbei um eine Templatereferenz handelt, und um einen validen Rückgabewert in den Methoden darzustellen.
@Path("/planets/ui")
public class PlanetResource {
[...]
record PlanetForm(Planet planet, Action action) implements TemplateInstance {}
@GET
@Path("/new")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getNewPlanetForm() {
return new PlanetForm(new Planet(), Action.NEW);
}
@GET
@Path("/{name}/edit")
@Produces(MediaType.TEXT_HTML)
public TemplateInstance getEditPlanetForm(@PathParam("name") String name) {
Optional<Planet> planetOpt = planetService.getPlanetByName(name);
if (planetOpt.isPresent()) {
return new PlanetForm(planetOpt.get(), Action.EDIT);
} else {
return Templates.error(404, "Planet '" + name + "' nicht gefunden");
}
}
[...]
}
src/main/java/de/gedoplan/showcase/web/PlanetResource.java
Hier kommen wir ohne das Keyword native
aus. Wichtig ist es hier auf Groß- und Kleinschreibung zu achten und wenn wir z.B. das Template Planet.html
verwenden möchten würde die Record Definition sich standardmäßig mit dem Namen der Model-Klasse Planet
überschneiden. Hier müssten wir entweder einen voll qualifizierten Namen verwenden, den Klassen- oder Dateinamen anpassen oder die Standardeinstellungen mit der Annotation @CheckedTemplate
anpassen.
Mit der Annotation @TemplateData
an der beim Rendern verwendeten Klasse Planet
können wir Qute anweisen einen sogenannten Resolver zu generieren um einen Zugriff über Reflection zur Laufzeit zu vermeiden. Für Enumerations gibt es die Annotation @TemplateEnum
, die es uns auch erlaubt im Template auf die Enum-Konstanten zuzugreifen.
@TemplateData
public record Planet(String name, double diameterKm, double distanceFromSunMillionKm,
boolean hasRings, int numberOfMoons, String description) { [...] }
src/main/java/de/gedoplan/showcase/model/Planet.java
// Zugriff auf Enum Konstanten im Template über Action:NEW
@TemplateEnum
public enum Action {NEW, EDIT}
src/main/java/de/gedoplan/showcase/web/PlanetResource.java
Methodenerweiterungen für Datenobjekte
Manchmal wäre es hilfreich, wenn wir bestehende Java-Klassen um neue Methoden erweitern könnten, wie man es z.B. aus Kotlin kennt. Zumindest für die Datenobjekte, die wir in den Templates verwenden, können wir mit Qute solche Methoden definieren. In diesem Fall möchten wir auf der Planetenseite gerne auch den Abstand zur Sonne in astronomischen Einheiten angeben. Wir könnten hier natürlich auch einfach unsere Klasse erweitern, aber insbesondere wenn die betreffende Klasse nicht unter unserer Kontrolle liegt können wir diesen Ansatz verwenden.
Wir definieren eine statische Methode mit der Annotation @TemplateExtension
die ein Planet
Objekt entgegen nimmt und einen double
Wert zurückgibt. In der Methode wird dann die entsprechende Berechnung vorgenommen. Bei der Ausgabe können wir auf eine weitere, bereits vordefinierte, Erweiterung zurückgreifen und über str:fmt()
die Ausgabe entsprechend formatieren.
@TemplateExtension
static double distanceFromSunAU(Planet planet) {
return planet.distanceFromSunMillionKm() / 149.597_870_700;
}
src/main/java/de/gedoplan/showcase/web/PlanetResource.java
{str:fmt("%.3f", planet.distanceFromSunAU)}
src/main/resources/templates/PlanetResource/planet.html
Das ganze Projekt findet ihr wie immer auch auf GitHub.