Bli
kjent
med
Spring
-
Kapittel
3:
Thymeleaf

Kapittel 3 - Template Engine


Introduksjon

I forrige kapittel ble det demonstrert hvordan Spring Boot kan brukes for å raskt og enkelt sette opp en fungerende Java Webserver. I samme demonstrasjon ble Thymeleaf benyttet som template engine for å bygge grensesnittet. En template engine er en motor som kan erstatte elementer i et HTML, CSS eller JavaScript-dokument (eller et hvilket som helst dokument) slik at det blir skreddersydd til situasjonen.


Det finnes flere kjente template engines. Java har blant annet Java Server Pages (JSP), FreeMarker og Thymeleaf. Til sammenligning finner du blant annet Razor og Active Server Pages (ASP) for .NET, Django for Python og Mustache kan brukes for alt annet.


Spring sitt web-rammeverk er bygget rundt MVC-modellen. MVC er en forkortelse for Model-View-Controller og det er et velkjent designmønster for å separere ansvarsområder i applikasjonen. Model har ansvar for dataene og View har ansvar for grensesnittet. Controller er et lag i mellom Model og View og sørger for at de kommuniserer korrekt og sikkert. En template engine brukes for å generere View i denne modellen.


I dette kapittelet ønsker jeg å gå mer i dybden på bruk av template engine i en Spring Boot applikasjon. Innledningsvis ønsker jeg å demonstrere hvordan Java Server Pages kan benyttes. Deretter fortsetter jeg med å sammenligne JSP-løsningen med Thymeleaf. Underveis vil jeg belyse fordelene ved å bruke en template engine basert på Natural Templates.


Kodeeksempler finnes her.


Java Server Pages

Et av de opprinnelige spesifikasjonene til J2EE i 1999 var Java Server Pages (JSP). Denne spesifikasjonen beskriver hvordan man kan generere dynamiske websider ved at Java-kode erstatter bestemte JSP-elementer i et HTML-dokument. Etter at Eclipse Foundation overtok ansvaret for Java Enterprise Edition (JavaEE) så har denne spesifikasjonen skiftet navn til Jakarta Server Pages.


Både i JSP og Thymeleaf-prosjektene skal vi bygge en enkel TODO-applikasjon. La oss ta en titt på hvordan man kan opprette en Spring Boot applikasjon med Java Server Pages.


Først og fremst henter du en Spring-Boot starter-applikasjon fra start.spring.io. Velg Spring Web som en dependency.
For å enkelt arbeide med JSP sammen med Spring Boot så kan det være en fordel å legge til følgende avhengigheter i pom.xml filen.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
  	<groupId>org.apache.tomcat.embed</groupId>
  	<artifactId>tomcat-embed-jasper</artifactId>
  	<scope>provided</scope>
</dependency>
<dependency>
  	<groupId>javax.servlet</groupId>
  	<artifactId>jstl</artifactId>
  	<version>1.2</version>
</dependency>


Spring Boot tilbyr en innebygd TomCat-container. Avhengighetene du nettopp la til forteller TomCat-containeren at den skal arbeide med JSP-filer.
Vanligvis plasseres JSP-filer i src/main/webapp/WEB-INF/jsp. Dette ønsker vi å gjøre også for vår Spring Boot-applikasjon, men fordi Spring Boot følger sine egne standarder er man nødt til å fortelle Spring hvor den skal lete etter template-filer. Skriv følgende linjer i filen application.properties:

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

I tillegg til dette er man nødt til å konfigurere Spring Boot sin SpringApplicationBuilder. Oppdater main-klassen til å arve fra SpringBootServletInitializer og opprett en ny metode: configure.

package no.progit.jspexample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class JspExampleApplication extends SpringBootServletInitializer {

	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
		return builder.sources(JspExampleApplication.class);
	}

	public static void main(String[] args) {
		SpringApplication.run(JspExampleApplication.class, args);
	}

}


Denne artikkelen er ikke ment for å beskrive detaljene rundt opprettelse av en Spring-Boot med JSP-View applikasjon, men stegene over er greit å få notert ned da det kan være til hjelp om man ønsker å forsøke selv. Jeg kan melde at jeg brukte litt tid på å debugge.
La oss ta en titt på en JSP-fil.

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
    <title>JSP todos</title>
    <link href="<c:url value="/css/common.css"/>" rel="stylesheet" type="text/css">
</head>
<body>
<c:if test="${addTodoSuccess}">
    <div>Successfully added Todo with ID: ${savedTodo.id}</div>
</c:if>
<table>
    <thead>
    <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Finished</th>
    </tr>
    </thead>
    <tbody>
    <c:forEach items="${todo}" var="todo">
        <tr>
            <td>${todo.id}</td>
            <td>${todo.name}</td>
            <td>${todo.finished}</td>
        </tr>
    </c:forEach>
    </tbody>
</table>
<c:url value="/todo/add" var="addTodoPageUrl" />
<a href=${addTodoPageUrl} >
    <button>Add new TODO</button>
</a>
</body>
</html>

Dette er filen view-todo.jsp. Denne filen skal ikke ligge i resources-mappen som man vanligvis forventer av template-filer i en Spring Boot-applikasjon, men i webapp/WEB-INF/jsp slik at det matcher pathen som anvist i application.properties tidligere.


For eksempel følgende kode vil kun vises dersom variabelen addTodoSuccess er true.

<c:if test="${addTodoSuccess}">
    <div>
        <p>Successfully added Todo with ID: ${savedTodo.id}</p>
    </div>
</c:if>

JSP kan også brukes til å iterere gjennom lister og bygge HTML-tagger iterativt.

<c:forEach items="${todo}" var="todo">
    <li>
        <p>${todo.id}</p>
        <p>${todo.name}</p>
        <p>${todo.finished}</p>
    </li>
</c:forEach>

Denne JSP-filen vil prosesseres av JSP sin template engine og vil resultere i et HTML-dokument som sendes til nettleseren. Denne prosessen forekommer i TomCat-containeren.
JSP-filen er strukturert som en HTML-fil, men byr på ekstra funksjonalitet. Først og fremst er det mulig å skrive Java-kode i JSP-filen som kan refereres til i HTML-taggene, men dette blir sett på som bad practice da det gjør det uoversiktlig å debugge. I vårt tilfelle blir JSP-filen forsynt med variabler via en Controller (ref. MVC-modellen).
La oss ta en titt på controlleren brukt til å sende data til JSP-filen:

package no.progit.jspexample.controller;

import no.progit.jspexample.domain.AddTodo;
import no.progit.jspexample.domain.Todo;
import no.progit.jspexample.service.TodoService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.springframework.web.servlet.view.RedirectView;

@Controller
@RequestMapping("/todo")
public class TodoController {

    private final TodoService todoService;

    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @GetMapping("/view")
    public String viewTodos(Model model) {
        model.addAttribute("todo", todoService.getTodos());
        return "view-todo";
    }

    @GetMapping("/add")
    public String viewAddTodo(Model model) {
        model.addAttribute("add-todo", new Todo());
        return "add-todo";
    }

    @PostMapping("/add")
    public RedirectView addTodo(@ModelAttribute("add-todo") AddTodo addTodo, RedirectAttributes redirectAttributes) {
        final RedirectView redirectView = new RedirectView("/todo/view", true);
        Todo savedTodo = todoService.addTodo(new Todo(addTodo.getName()));
        redirectAttributes.addFlashAttribute("savedTodo", savedTodo);
        redirectAttributes.addFlashAttribute("addTodoSuccess", true);
        return redirectView;
    }
}

Klassen TodoController sørger for å gi nødvendig informasjon til motoren som genererer HTML fra JSP-filen. Controlleren er koblet til to websider, /todo/view og /todo/add. Førstnevnte henter en liste med alle lagrede TODO og gir disse til Model som igjen forsyner template engine. Listen hentes fra en tjeneste som heter TodoService og vil i denne sammenhengen representere modellen i MVC. Dette er ofte en database eller en form for lagringsmedium.
Sistnevnte (/todo/add) er programmert til å lytte på både en GET og en POST-request. GET-requesten sender template engine en webside med et tomt TODO-objekt, klart til å populeres. POST-requesten mottar et AddTodo-objekt generert av websiden og lagrer dette. Deretter blir resultatet sendt til en modell og videresendt tilbake til /todo/view.


Dette er definitivt et praktisk verktøy for å generere HTML-sider. Det kan tenkes at det var et enda viktigere verktøy da spesifikasjonen ble publisert i 1999. Denne funksjonaliteten bidrar til å kunne lage dynamiske websider basert på situasjonen.Det er dog noen aspekter ved JSP som har gitt grobunn for alternative template engines.


JSP og forbedringspotensiale

Forestill deg at du arbeider på et stort prosjekt som inneholder flere JSP-filer som er komplekse og krever mange variabler. En typisk skjema-applikasjon. Du får en oppgave om å utføre en liten justering på designet. Enkelt og greit. Du utfører de nødvendige designendringene i filen. Neste steg er å teste om endringene er ihenhold til oppgavebeskrivelsen.
Å nei! Nettleseren din kan ikke tolke jSP-filer. Alt ser helt korrupt ut! For å teste at den lille designendringen er i henhold til oppgavebeskrivelsen må du starte java-applikasjonen i TomCat-containeren som må prosessere JSP-filen slik at du sitter igjen med HTML som kan tolkes av nettleseren.
Hvis dette i tilegg er en side i applikasjonen som krever innlogging, trykk og/eller flere tilstander for å vises så gjør det testingen enda mer tungvint. Typisk, du gjorde en liten skrivefeil! Du utbedrer og går igjennom alle stegene over på nytt. Dette gjøres for hver endring og det er tidskrevende.

Ja OK, eksempelet over er kanskje lit ekstremt, men det er en svakhet ved å bruke en template-engine som kun kan tolkes i et bestemt miljø. Det finnes verktøy som jRebel som restarter applikasjonen automatisk, men det er som et lite plaster på såret. La oss sammenligne JSP med en nyere template engine, Thymeleaf.


Thymeleaf

Thymeleaf ble skapt av Daniel Fernández i 2011 og bygger på konseptet om Natural Templates.


My HTML is not HTML anymore, but that's OK because the UI design phase is already finished.

Web developers, lying to themselves - since epoch


Thymeleaf er, i likhet med JSP, en template engine, men de baserer sin motor på Natural Templates. Det betyr at templaten kan tolkes direkte av en nettleser som en prototype. For å få til dette brukes non-standard attributter i HTML-dokumentet som nøkkelord som Thymeleaf tolker. Dette er elementer som ikke blir tolket av nettleseren i det hele tatt. Nettleseren aner ikke hva det er, men det er gyldig og velger derfor å ignorere det. Dette åpner opp for å kunne skrive templates som fungerer både til prototyping samt produksjonskode. Thymeleaf-templates er nemlig helt ordinære HTML-filer med .html filtype. La oss se på de tilsvarende eksemplene som med JSP-filen over.

<div th:if="${addTodoSuccess}">
    <p th:text="'id: ' + ${savedTodo.id}">Id: 4444</p>
</div>
<ul th:each="todo: ${todos}">
    <li>
        <p th:text="'ID: ' + ${todo.id}">ID: 123</p>
        <p th:text="'Description: ' + ${todo.description}">Description: 123</p>
        <p th:text="'Finished: ' + ${todo.finished}">Finished: 123</p>
    </li>
</ul>

Istedet for å skrive <c:if> for å vilkårlig vise et <div>-element, så skrives det i Thymeleaf <div th:if ...> i Thymeleaf. Det samme gjelder ved iterering. Istedet for å skrive <c:forEach> så skriver vi <ul th:each .... Den store fordelen med denne løsningen er at om du åpner HTML-filen så vil ikke nettleseren bry seg om feltene th:if og th:each i HTML-filen da den ikke vet hva den skal gjøre med denne informasjonen.


Forestill deg at du får en ny oppgave om å utføre en liten designendring i et stort prosjekt. Denne gangen er det brukt Thymeleaf som template engine i View-laget. I dette prosjektet kan du utføre endringene og verifisere dette direkte i nettleseren din. Du trenger ikke starte TomCat-containeren eller navigere deg til riktig webside. Herlig!


Thymeleaf prototyping

Du la kanskje merke til at det allerede er informasjon i Thymeleaf-taggene. For eksempel: <p th:text="'ID: ' + ${todo.id}">ID: 123</p>. Når Thymeleaf sin template engine prosesserer filen så vil den bytte ut ID: 123 med hva enn variabelen todo.id er. Dersom du åpner den direkte i en nettleser så vil den vise ID: 123. Dette gir utviklerne anledning til å prototype og gjøre designendringer uten å involvere backend. Enkelt.


Hva med gjenbruk?

Gjenbruk av komponenter er viktig for å redusere mengde kode. Ved å redusere mengde kode så reduserer du teknisk gjeld og reduserer risikoen for bugs som er gjemt i mengden. Dette gjelder også for View-laget. Thymeleaf støtter gjenbruk av HTML-dokumenter via fragments og parameterized fragments.
La oss bygge en HTML-fil (page-header.html) som kun inneholder overskriften/headeren til siden som kan gjenbrukes.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Header fragment</title>
</head>
<body>
    <div th:fragment="header">
        <div class="page-header">
            <h1>Progit Thymeleaf TODO</h1>
        </div>
    </div>
</body>
</html>


Dette er et fullstendig HTML-dokument som kan stå helt fint på egne bein. Legg merke til syntaksen th:fragment="header". Dette betyr kort fortalt eksporter denne delen med navn header. Denne kan vi deretter importere på hovedsiden slik:


<header th:insert="page-header.html :: header"></header>


Det er også mulig å eksportere flere kodesnutter fra en og samme fil. th:insert syntaksen leter etter th:fragmentmed en gitt ID fra filen som blir oppgitt.
Dette ligner nesten litt på hvordan ulike JavaScript-rammeverk som React og Angular gjør det. Men disse rammeverkene har jo fordelen med render-props. Støtter Thymeleaf dette? Det er sikkert og visst, og det heter parametrized fragments.


Parameterized Fragments

Parameterized fragments er fragments men som muliggjør å motta data fra filen som bruker den. Her er et eksempel på HTML-fragment som tar imot parametere.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Label and input parameterized fragment</title>
</head>
<body>
    <div th:fragment="inputFunksjon (name, id)">
        <label th:for="${name}" th:text="${name}" for="description">Description</label>
        <input
                type="text"
                th:id="${id}"
                id="description"
                th:field="*{__${id}__}"
                th:placeholder="'Enter ' + ${name}"
        />
    </div>
</body>
</html>


Denne kodesnutten eksporterer fragmenten inputFunksjon som tar inn to parametere, name og id. Disse brukes til å generere enkle label og input-tagger.For å bruke denne parameteriserte fragmenten så kan man importere den på følgende måte:

<div th:replace="label-and-input.html :: inputFunksjon(type='text', name='Description', id='description')"></div>`


Hva med JavaScript?

Det er ikke kun HTML-dokumenter Thymeleaf kan arbeide med. Thymeleaf melder om følgende templates:

  • HTML
  • XML
  • TEXT
  • JavaScript
  • CSS
  • RAW

Du kan bygge gyldig JavaScript ved å bruke følgende syntaks:

<script th:inline="javascript">
    var username = [[${session.user.name}]];
</script>


session.user.name blir erstattet med parameteren du sender til Thymeleaf motoren. Prototyping er også tilgjengelig for JavaScript. Dette har de løst ved å tolke kommentert JavaScript som template. Alt etter den kommenterte linjen og frem til første semikolon blir erstattet med parameteren din av Thymeleaf-motoren. Når scriptet blir kjørt direkte av nettleseren så vil den kommenterte delen naturligvis bli ignorert. Da sitter du igjen med en god default-verdi til tesitng.Se eksempel:

<script th:inline="javascript">
    var username = /*[[${session.user.name}]]*/ "Progit Consulting User";
</script>


Kontakt

Ikke nøl med å kontakt meg på oya@progit.no dersom du har tilbakemeldinger.


Kilder

Jeg har brukt en del tid på å lese dokumentasjon, programmere og sett videoer i forbindelse med denne artikkelen.

Først og fremst anbefaler jeg presentasjonen fra skaperen av Thymeleaf, Daniel Fernandéz.
Daniel Fernandéz prater om Thymeleaf

Deretter vil jeg berømme nettsiden til Thymeleaf for god dokumentasjon og gode eksempler: https://www.thymeleaf.org