Thymeleaf como alternativa MVC

 

Hace 5 años escribí por primera vez sobre Thymeleaf y hoy, con el proyecto ya bastante más maduro, vuelvo a hacerlo de nuevo en estas líneas: Qué es, qué ventajas ofrece, y un sencillo ejemplo práctico.

Thymeleaf es un motor de plantillas Java para aplicaciones, tanto web como standalone, construido sobre estándares HTML5, por lo que es compatible con la inmensa mayoría de navegadores. Entre sus principales características cabe destacar su flexibilidad y su alto rendimiento que, junto con su facilidad de aprendizaje y su poco intrusismo en el marcado lo convierten en una alternativa real a otras opciones más populares como JSP.

Para los que trabajamos con Spring Framework, Thymeleaf nos ofrece un set completo de integración que nos permite incorporalo dentro del patrón MVC:

  • Los @Controller de Spring pueden redireccionarse a las plantillas Thymeleaf de la misma forma que, por defecto, se hacen a JSP.

  • Permite el uso de Spring Expression Language (Spring EL)

  • Los formularios se integran perfectamente con los beans y los result bindings de Spring, así como con los manejadores de errores.

  • Permite internacionalización a través de los típicos MessageSource de Spring.

Con todo esto, y como veremos más adelante en el ejemplo, la integración de Thymeleaf con Spring es tremendamente sencilla y casi transparente para el desarrollador.

Como curiosidad, durante la SpringOne de 2014 el equipo de Pivotal recomendó el uso de Thymeleaf como motor de plantillas y, para demostrar su integración, anunciaron que la propia web de spring.io está construida con Thymeleaf. Desde entonces, esa recomendación se repite en muchos de los nuevos tutoriales del site oficial de Spring Framework.

Ok, basta de teoría. Vamos a ver cómo funciona. En este post mostraré un ejemplo muy sencillo porque, de forma paralela a mi proyecto actual, he arrancado un side project basado en Thymeleaf que espero que acabe dando sus frutos en forma de webinar o bien de un post más avanzado sobre plantillas en las próximas semanas (aprovecho para lanzarle el guante a mis compañeros ;).

En este ejemplo, vamos a crear una pequeña app para realizar sorteos. En la pantalla inicial se deberá rellenar un formulario con una lista de participantes así como el número de participaciones que tiene cada uno. Al hacer el submit, devolveremos una tabla con todos los participantes ordenados aleatoriamente, teniendo en cuenta que, a mayor número de participaciones, más probabilidades tiene de estar más arriba.

Las fuentes del proyecto podéis descargarlas en mi Github: https://github.com/sebascastillo89/thymeleafdemo

 

(1) Proyecto Maven y Spring Boot

Generamos un proyecto con Spring Boot a través de Spring Initializr, indicando como dependencias Web, que incorporará capacidades Spring MVC y un servidor Tomcat embebido a nuestra aplicación y Thymeleaf, que incorporará nuestro motor de plantilla:

thymeleaf_project.png

Una vez generado el proyecto, podemos descomprimirlo, compilarlo usando Maven e importarlo en nuestro IDE Eclipse.

(2) Controlador

Vamos a crear el controlador que implemente la lógica de nuestro sorteo, RaffleController:


package es.secaro.thymeleafdemo.controller;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;

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 es.secaro.thymeleafdemo.dto.Raffle;

@Controller
public class RaffleController {
	
	@GetMapping("/raffle")
	public String raffleForm(Model model) {
		model.addAttribute("raffle", new Raffle());
		return "raffle";
	}
	
	@PostMapping("/raffle")
	public String raffleSubmit(@ModelAttribute Raffle raffle) {
		
		String candidates = raffle.getCandidates();		
		List winners = doRaffle(asList(candidates));

		raffle.setWinners(winners);
		return "result";
	}

	private List doRaffle(List candidates) {
		
		Random random = new Random();
		List winners = new ArrayList();		
		
		int winnerSize = new HashSet(candidates).size(); // HashSet elimina duplicados		
		while(winnerSize>0){
			String nextWinner = candidates.get(random.nextInt(candidates.size()));	
			while(winners.contains(nextWinner)){
				nextWinner = candidates.get(random.nextInt(candidates.size()));	
			}
			winners.add(nextWinner);
			winnerSize--;
		}
		return winners;
	}

	private List asList(String candidates) {
		
		List candidatesAsList = new ArrayList();
			for (String line : candidates.split("\\n")) {
				
				String[] split = line.split(",");
				String name = split[0].trim();
				int tickets = Integer.valueOf(split[1].trim());

				while (tickets > 0) {
					candidatesAsList.add(name);
					tickets--;
				}
			}
			return candidatesAsList;		
	}

}

Para que nuestro controlador intercepte las peticiones HTTP basta con decorar nuestra clase con @Controller.

Cuando accedamos a través del navegador (petición GET) a /raffle el método raffleForm() decorado con @GetMapping interceptará la petición y añadira un objeto Raffle a nuestro componente modelo de MVC.

Cuando hagamos submit de nuestro formulario (petición POST) el método raffleSubmit() decorado con @PostMapping interceptará la petición, realizará el sorteo y añadirá la lista ordenada de ganadores al modelo.

Ambos métodos controladores devuelven una cadena de texto (p.e: return "raffle";). Dentro del patrón MVC (Modelo Vista Controlador) esta cadena se utilizará para identificar la vista que queremos procesar (en este caso raffle.html).

Los métodos doRaffle y asList son dos métodos privados de utilidad que realizan el sorteo y convierten la lista de candidatos en un objeto List<String> respectivamente. 

Por último, para representar a nuestro modelo usamos el objeto Raffle, que es un simple POJO que contiene la cadena de candidatos que tomamos como entrada y la lista ordenada de ganadores que mostramos a la salida:


package es.secaro.thymeleafdemo.dto;

import java.util.List;

public class Raffle {

	private String candidates;
	private List winners;

	public String getCandidates() {
		return candidates;
	}

	public void setCandidates(String candidates) {
		this.candidates = candidates;
	}

	public List getWinners() {
		return winners;
	}

	public void setWinners(List winners) {
		this.winners = winners;
	}

} 

 

(3) raffle.html

Vamos con nuestra pantalla de inicio, dóide únicamente recibiremos la lista de candidatos y el número de participaciones o tickets que tiene cada uno. Evidentemente, a mayor número de participaciones más probabilidades tendrán de estar en lo más alto de la tabla de ganadores. Veremos como el binding o unión de nuestro formulario con nuestro controlador y con nuestro modeloes bastante sencillo:

thymeleaf_raffle.png

En primer lugar, th:action="@{/raffle}" envía el submit de nuestro formulario mediante un POST a /raffle, que es la URL que tenemos configurada para que intercepte nuestro Controlador (ver @PostMapping en RaffleController).

En segundo lugar, th:object="${raffle}" declara el objeto modelo contra el que haremos el binding para recopilar los datos del formulario, en este caso Raffle.java

Por último, th:field="*{candidates}" hace el binding entre el campo del formulario anotado y el campo candidates de nuestro objeto modelo.

 

(4) result.html

Por último, vamos a generar nuestra página de respuesta result.html, donde mostraremos la tabla de todos los participantes ordenados por sorteo, teniendo más probabilidad de estar arriba (ganador) aquellos que más participaciones tienen.

Aprovecharemos este pequeño ejemplo para ver cómo iterar una lista con Thymeleaf:

thymeleaf_result.png

Usamos th:each="winner: ${raffle.winners}" para iterar la List de nuestro modelo Raffle. Añadimos una nueva fila a nuestra tabla con cada uno de los elementos de esa lista con th:text="${winner}".

 

(5) Probando nuestra app.

Al tratarse de un proyecto Spring Boot con un servidor tomcat embebido, bastará con compilar nuestro proyecto (mvn package) y ejecutar el jar generado (java -jar target/thymeleafdemo-0.0.1-SNAPSHOT.jar). Ambos comandos se deben ejecutar sobre la raíz del proyecto.

Para saber más sobre cómo generar, compilar y desplegar una aplicación con Spring Boot os recomiendo echar un vistazo al webinar: Construyendo un API Rest con Spring, MongoDB & Heroku

Si vamos directamente a localhost:8080/raffle veremos nuestro formulario, donde podemos ejecutar un ejemplo:

thymeleaf_demoRaffle.png

Si realizamos el sorteo, obtenemos la tabla de ganadores:

thymeleaf_demoResults.png

Hasta aquí este sencillo ejemplo de integración de Thymeleaf con Spring MVC. En próximos post daremos un paso más en el marcado con Thymeleaf y conoceremos la suite de pruebas de la herramienta.

Puede encontrar las fuentes del ejemplo aquí: https://github.com/sebascastillo89/thymeleafdemo