Variables de ruta de Thymeleaf con Spring Boot

En esta guía, veremos cómo usar variables de ruta en los controladores Spring Boot, así como también cómo generar enlaces con variables de ruta con Thymeleaf.

Introducción

Thymeleaf es un motor de plantillas (representación del lado del servidor) utilizado por muchos ingenieros de software Java dentro de las aplicaciones web basadas en Spring. Una característica importante de cualquier aplicación web es la compatibilidad con direcciones URL dinámicas y variables de ruta dentro de esas direcciones URL.

La mayoría de las API REST extensamente usan variables de ruta para especificar las ID de los elementos en los que están realizando operaciones. Por ejemplo, un ejemplo típico sería:

1
2
3
https://www.somewebsite.com/viewPost/path-variables-with-spring-boot
# OR
https://www.somewebsite.com/viewProduct/5

En ambos casos, estamos tratando de encontrar un recurso indicado por un determinado identificador. En el primer caso, estamos identificando un recurso por su título - path-variables-with-spring-boot, mientras que en el segundo, lo estamos identificando a través de un contador de ID incremental - 5.

Nota: Cuando utilice variables de ruta predecibles, como un contador incremental, tenga cuidado con los problemas de seguridad. En primer lugar, estos son fácilmente eliminables, pero lo más importante es que, sin la validación adecuada, alguien podría darse cuenta de que /deleteProduct/5 elimina las entradas de la base de datos y decidir eliminar la mayoría de las entidades almacenadas en ella.

En esta guía, veremos cómo aprovechar Thymeleaf para recuperar variables de ruta y cómo usar los controladores Spring Boot para procesarlas.

A lo largo de la guía, usaremos un modelo de Publicación, que representa una publicación de blog, que solo tiene una identificación y algo de contenido:

1
2
3
4
5
6
7
8
9
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "post_sequence")
    private Long id;
    private String content;

    // Constructors, Getters, Setters, and toString 
}

Ya que estamos usando Spring Boot, también arranquemos un PostRepository basado en JpaRepository, que nos permite realizar operaciones CRUD listas para usar:

1
2
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {}

Variables de ruta de Thymeleaf con Spring Boot

Algunas URL son dinámicas, es decir, dado el hecho de que podemos crear, leer, actualizar y eliminar nuestras entidades Publicar, querremos tener URL dinámicas para las solicitudes GET, UPDATE y DELETE.

Inicialicemos nuestro proyecto con algunas publicaciones artificiales:

1
2
3
4
5
6
7
8
9
@GetMapping("/initialize")
public ResponseEntity<String> initialize() {
    Post post1 = new Post("Content of post 1");
    Post post2 = new Post("Content of post 2");
    Post post3 = new Post("Content of post 3");
    postRepository.saveAll(List.of(post1, post2, post3));
    
    return ResponseEntity.ok("Initialized posts");
}

Una vez que lleguemos a nuestro punto final /initialize, se generarán tres publicaciones y se guardarán en la base de datos. Ahora, definamos una URL estática para que el usuario recupere todas las publicaciones:

1
2
3
4
5
6
7
@GetMapping("/viewPosts")
public String viewAllPostsAndComments(Model model){
    List<Post> postList = postRepository.findAll();
    model.addAttribute("postList", postList);
    
    return "all-posts";
}

Esta es una URL estática y un controlador: no hay variables de ruta o parámetros que puedan permitir que el usuario influya en qué publicaciones se están recuperando. Por lo general, esto es lo que desea si desea permitir que el usuario elija dónde desea navegar. Una vez elegidos, pueden ver la lista detallada de cualquier publicación, simplemente haciendo clic en ella y navegando a su página.

Dado que sería poco práctico (e inviable) crear un controlador de solicitudes para cada publicación, podemos crear un controlador dinámico que acepte cualquier ID de publicación, encuentre la publicación en la base de datos y la devuelva:

1
2
3
4
5
6
7
@GetMapping("/viewPost/{postId}")
public String viewPost(@PathVariable("postId") Long postId, Model model) {
    Post post = postRepository.findById(postId).get();
    model.addAttribute("post", post);
    
    return "view-post";
}

Aquí, hemos definido un @GetMapping para la URL /viewPost/{postId}. El postId entre corchetes es una variable dinámica que se puede establecer en cualquier valor y/o tipo. En la firma de nuestro método, hemos utilizado la anotación @PathVariable, configurando el nombre de la variable de ruta y asignándolo a un tipo al que podemos hacer referencia: Long postId.

Nota: El nombre de la variable de ruta en la anotación @GetMapping tiene que coincidir con el nombre que hemos definido en la anotación @PathVariable. Puede usar tantas variables de ruta como desee en una URL y unirlas a través de sus nombres.

Una vez que se activa una solicitud GET, digamos, el punto final /viewPost/5: el postId se convierte implícitamente en Long y podemos usarlo para buscar la publicación a través de su ID en la base de datos. Sin embargo, si pasamos otro tipo, como String - /viewPost/some-post, este controlador generará una excepción:

1
java.lang.NumberFormatException: For input string: "some-post"

Dado que tenemos los controladores para manejar tanto una solicitud para ver todas las publicaciones como para ver una sola, escribamos rápidamente las páginas de Thymeleaf que nos permiten navegar a través de estos controladores de solicitudes. Comencemos primero con una página que enumera todas las publicaciones y permite al usuario navegar a los diferentes controladores de solicitudes de controladores:

1
2
3
4
<div th:each="post : ${postList}">
   <p th:text="${post.content}"></p>
   <a class="btn btn-info" th:href="@{/viewPost/{id}(id = ${post.id})}">View Post</a>
</div>

Aquí, iteramos sobre cada publicación en postList (lista de publicaciones que agregamos a la instancia de Modelo) y para cada una, agregamos un botón que permite al usuario ver la publicación. El atributo href del enlace nos lleva a:

1
th:href="@{/viewPost/{id}(id = ${post.id})}

Esta es la sintaxis de URL estándar para agregar parámetros en Thymeleaf. La expresión estándar @{} utilizada para enlaces también acepta variables de ruta. El {id} es una variable de ruta, que podemos configurar externamente. Aquí, lo hemos apuntado a post.id. Una vez representada en el lado del servidor, esta expresión se evalúa como:

1
<a href="localhost:8080/viewPost/1" class="btn btn-info" >View Post</a>

Al hacer clic en este botón, se activará el punto final /viewPost/{postId}, con el valor postId convertido en 1 y el postRepository buscará la publicación con el id de 1, devolviéndola a la vista view-post Thymeleaf:

1
2
3
4
5
6
7
<div class="container">
    <div class="row">
        <div class="col-md-12 bg-light">
            <p th:text="${post.content}"></p>
        </div>
    </div>
</div>

Conclusión

Las variables de ruta son una característica común y clave de las API REST. En esta guía, hemos analizado cómo usar variables de ruta en Spring Boot con Thymeleaf.