Spring MVC con Spring Boot + Thymeleaf

El patrón Modelo Vista Controlador (MVC) es un patrón de arquitectura de software que separa la lógica de la aplicación (el modelo) de su representación (la vista) y para la interacción entre ambas introduce el controlador que se encarga de trasladar las solicitudes provenientes de la vista a la aplicación para que esta haga lo que corresponda y luego le devuelva el resultado al controlador y que sea este el que le envié a la vista el resultado para que lo represente.

El objetivo del patrón MVC es separar la vista de la lógica de la aplicación para facilitar la reutilización de código y simplificar el desarrollo de ambas capas de forma separada para facilitar el cambio de la vista o para crear aplicaciones con múltiples vistas (web, android, ios, pdf, …).

Crear aplicación Spring MVC con Spring Boot

Para crear un proyecto con Spring Boot para usar Spring MVC simplemente tenemos que añadir como dependencia a nuestro proyecto el spring-boot-starter-web y la dependencia al motor de plantillas que queramos utilizar si no queremos usar las típicas jsp, en este ejemplo vamos a usar Thymeleaf (spring-boot-starter-thymeleaf) que nos permite tener una integración total con Spring y una sintaxis más cercana al HTML porque en lugar de añadir nuevas etiquetas lo que hace es añadir nuevos atributos a las etiquetas típicas de HTML5.

Si no sabes cómo hacer este paso échale un vistazo a este artículo en el que creamos la aplicación que vamos a usar como base aquí.

Spring MVC

Vamos a ver cómo implementar el MVC con Spring usando en la parte de la vista Thymeleaf.

Spring MVC: El Modelo

El modelo van a ser nuestros propios objetos por lo tanto Spring no nos va a limitar ni ayudar en este aspecto, pero para poder enviarle los objetos a la vista y que esta pueda identificarlos y saber que tiene que hacer con ellos nos ofrece Model, ModelMap y ModelAndView. Las 2 primeras basicamente podemos decir que son iguales pero mientras ModelMap es una clase, Model es una interfaz y ModelAndView reúne en un solo objeto el modelo (un ModelMap) y la vista a la que se quiere enviar.

En los siguientes 3 métodos podemos ver un ejemplo de cómo se usan y todos producirán el mismo resultado (enviar a la vista «lista» el nombre de la aplicación y una lista de productos).


@GetMapping(value = "/lista")
public String listar(Model model) {
    model.addAttribute("titulo", nombreAplicacion);
    model.addAttribute("productos", productoService.obtenerTodosProductos());
    return "lista";
}

@GetMapping(value = "/listaModelMap")
public String listarModelMap(ModelMap model) {
    model.addAttribute("titulo", nombreAplicacion);
    model.addAttribute("productos", productoService.obtenerTodosProductos());
    return "lista";
}

@GetMapping(value = "/listaModelAndView")
public ModelAndView listarModelAndView() {
    ModelAndView mav = new ModelAndView();
    mav.addObject("titulo", nombreAplicacion);
    mav.addObject("productos", productoService.obtenerTodosProductos());
    mav.setViewName("lista");
    return mav;
}

Como se puede apreciar funcionan como un Map, porque más o menos es lo que son y de hecho podemos sustituirlos por un Map sin problemas, donde la clave es nombre que tendremos que usar para acceder al objeto desde la vista.

Spring MVC: La vista

En este ejemplo para la vista utilizamos Thymeleaf por lo que si quieres utilizar otro motor de plantillas o las míticas jsp la sintaxis será distinta pero gracias a la utilización del patrón MVC los cambios que necesitaríamos hacer para usar otra tecnología no van a afectar al resto de nuestra aplicación.

Aunque Thymeleaf es bastante simple de utilizar solo vamos a verlo un poco por encima para entender como interactúa con el controlador y dejo para un post distinto una vista más en detalle.


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout/layout::head">
    <meta charset="UTF-8">
</head>
<body>
    <header th:replace="layout/layout::header"></header>

    <div class="container">
        <div class="card bg-dark">
            <div class="card-header">
                <h3>Lista de productos</h3>
            </div>
            <div class="card-body">
                <table class="table table-striped">
                    <thead>
                        <tr>
                            <th>Id</th>
                            <th>Código</th>
                            <th>Nombre</th>
                            <th>Precio</th>     
                            <th></th>                   
                        </tr>
                    </thead>
                    <tbody>
                        <tr th:each="producto: ${productos}">
                            <td th:text="${producto.id}"></td>
                            <td th:text="${producto.codigo}"></td>
                            <td th:text="${producto.nombre}"></td>
                            <td th:text="${producto.precio}"></td>
                            <td><a th:href="@{eliminar/} + ${producto.id}" th:text="'Eliminar'" class="btn btn-outline-danger"></a></td>
                        </tr>
                    </tbody>
                    <tfoot>
                        <tr>
                            <td colspan="5">
                                <a th:href="@{crear}" class="btn btn-outline-primary btn-block">Añadir Producto</a>
                            </td>
                        </tr>
                    </tfoot>
                </table>
            </div>
        </div>
    </div>

    <footer th:replace="layout/layout::footer" class="bg-dark"></footer>
</body>
</html>

El código anterior es el correspondiente a la vista que representa la lista de productos de nuestro ejemplo en una tabla, básicamente es HTML con algunos atributos «raros» que comienzan con th: (th:replace, th:each, th:text, th:href) que son los propios de Thymeleaf y dentro de estas hay «cosas» que están dentro de ${…} o de @{…}.

El th:each sirve para iterar sobre listas de objetos (sí el ${…} es para acceder a los objetos) y repetirá el bloque de código dentro de la etiqueta en la se encuentra tantas veces como sea necesario (en este caso pinta una nueva fila de la tabla).

Con th:text podemos mostrar el texto correspondiente al objeto o propiedad de este y con th:href podemos crear enlaces y para no tener que indicar la url completa utilizaremos @{…} para solo tener que indicar la ruta relativa desde donde nos encontremos.

Y a continuación tenemos un formulario con otros 3 nuevos atributos (th:action, th:object y th:field), el th:action se corresponde al action típico de HTML, en th:object ponemos el objeto queremos rellenar (el que enviaremos al controlador) y con th:field rellenamos sus campos.


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="layout/layout::head">
    <meta charset="UTF-8">
</head>
<body>
    <header th:replace="layout/layout::header"></header>
    
    <div class="container">
        <div class="card bg-dark">
            <div class="card-header">
                <h3>Nuevo producto</h3>
            </div>
            <div class="card-body">
                <form th:action="@{guardar}" th:object="${producto}" method="post">
                    <!-- <input type="hidden" th:field="${producto.id}"> -->
                    <input type="hidden" th:field="*{id}" />
        
                    <div class="form-group row">
                        <label class="col-sm-2 col-form-label">Código</label>
                        <div class="col-sm-6">
                            <!-- <input type="text" th:field="${producto.nombre}"> -->
                            <input type="text" th:field="*{nombre}">
                        </div>
                    </div>
                    <div class="form-group row">
                        <label class="col-sm-2 col-form-label">Nombre</label>
                        <div class="col-sm-6">
                            <!-- <input type="text" th:field="${producto.codigo}"> -->
                            <input type="text" th:field="*{codigo}">
                        </div>
                    </div>
                    <div class="form-group row">
                        <label class="col-sm-2 col-form-label">Precio</label>
                        <div class="col-sm-6">
                            <!-- <input type="text" th:field="${producto.precio}"> -->
                            <input type="text" th:field="*{precio}">
                        </div>
                    </div>
                    <div class="form-group">
                        <div class="col-sm-6">
                            <input type="submit" value="Añadir producto" class="btn btn-outline-primary">
                            <a th:href="@{lista}" class="btn btn-outline-danger">Cancelar</a>
                        </div>
                    </div>
                </form>
            </div>
        </div>
    </div>
    
    <footer th:replace="layout/layout::footer" class="bg-dark"></footer>
</body>
</html>

Cuando usamos th:object en las etiquetas hijas podemos usar *{…} para acceder directamente a sus propiedades sin tener que repetir una y otra vez el nombre del objeto.

Spring MVC: El controlador

El controlador es el punto de unión entre el modelo y la vista y como su nombre indica se encarga de controlar la vista que tiene que mostrar, los datos que se incluirán en esta y de recopilar los datos enviados desde la vista para actuar en consecuencia (guardarlos, hacer una consulta, etc.).

Para crear un controlador en Spring simplemente tenemos que añadir la anotación @Controller a una clase java y anotar los métodos que queramos relacionar con las peticiones http con @RequestMapping o con alguna de sus abreviaturas (@GetMapping, @PostMapping, …).

Vamos a ver el controlador de nuestro ejemplo para tener una perspectiva global de lo que vamos a ver para que al ir explicando cada cosa concreta nos sea más sencillo.


@Controller
@RequestMapping("productos")
public class ProductoController {
    
    public static final String VISTA_LISTA = "lista";
    public static final String VISTA_FORMULARIO = "formulario";
    
    @Value("${aplicacion.nombre}")
    private String nombreAplicacion;

    @Autowired
    private ProductoService productoService;
    
    @GetMapping(value = "/lista")
    public String listar(Model model) {
        model.addAttribute("titulo", nombreAplicacion);
        model.addAttribute("productos", productoService.obtenerTodosProductos());

        return VISTA_LISTA;
    }
    
    @GetMapping(value = "/listaModelMap")
    public String listarModelMap(ModelMap model) {
        model.addAttribute("titulo", nombreAplicacion);
        model.addAttribute("productos", productoService.obtenerTodosProductos());

        return VISTA_LISTA;
    }
    
    @GetMapping("/listaModelAndView")
    public ModelAndView listarModelAndView() {
        ModelAndView mav = new ModelAndView();
        mav.addObject("titulo", nombreAplicacion);
        mav.addObject("productos", productoService.obtenerTodosProductos());
        mav.setViewName(VISTA_LISTA);

        return mav;
    }
    
    @GetMapping("/crear")
    public String crear(Map model) {
        Producto producto = new Producto();
        model.put("producto", producto);
        model.put("titulo", nombreAplicacion);
        
        return VISTA_FORMULARIO;
    }
    
    @PostMapping("/guardar")
    public String guardar(Producto producto) {
        productoService.guardar(producto);
        
        return "redirect:" + VISTA_LISTA;
    }
    
    @GetMapping("/eliminar/{id}")
    public String eliminar(@PathVariable(value="id") Integer idProducto) {
        productoService.eliminar(idProducto);

        return "redirect:../" + VISTA_LISTA;
    }

}

Nuestro ejemplo de controlador tiene 2 anotaciones sobre la clase, @Controller para que Spring sepa que es un controlador y @RequestMapping("productos"), esta segunda anotación aunque no es obligatoria es muy recomendable porque lo normal es que todas las urls de un mismo controlador empiecen igual para poder identificar claramente a que controlador estamos llamando y que es lo que hace y para evitar tener que poner esa primera parte de la url en cada uno de los métodos se puede poner en una anotación sobre el controlador. Por lo tanto la url para ver la lista de productos correspondiente al método listar de nuestro controlador sería http://localhost:9876/miapp/productos/lista.

Aunque se puede utilizar la anotación @RequestMapping(value="...", method = RequestMethod.GET) e indicar con el atributo method el tipo de request que soporta el método por simplicidad y comomidad se pueden usar las anotaciones @GetMapping, @DeleteMapping, @PostMapping y @PutMapping.

Tanto si elegimos usar @RequestMapping como si optamos por las anotaciones especificas el parámetro que siempre vamos a usar es value con el que indicamos cual es la url que vamos a gestionar con cada método, pero como puedes ver en el ejemplo si queremos podemos escribir directamente el valor sin poner el nombre del parámetro.

La última anotación del ejemplo que nos queda por ver es @PathVariable que como su nombre indica sirve para indicar que el atributo sobre el que se usa se va a rellenar con el valor con ese nombre que esta entre {} en la url.

Aunque en nuestro ejemplo solo hay una variable de este tipo no hay ningún impedimento para que podamos tener todos los que queramos aunque no es muy recomendable pasar demasiados parámetros de esta forma.


@GetMapping("/eliminar/{categoria}/{id}")
public String eliminar(@PathVariable(value="categoria") Integer idCategoria, @PathVariable(value="id") Integer idProducto) {
    productoService.eliminar(idCategoria, idProducto);

    return "redirect:../" + VISTA_LISTA;
}

La alternativa a pasar las variables como parte de la url cuando usamos peticiones de tipo GET es @RequestParam para pasar parámetros como toda la vida con ?nombre=valor&otra=x&…, si optamos por esta opción nuestro método eliminar quedaría así:


@GetMapping(value="/eliminar")
public String eliminar2(@RequestParam(value="id") Integer idProducto) {
    productoService.eliminar(idProducto);

    return "redirect:" + VISTA_LISTA;
}

Como habrás notado ahora el parámetro ya no se incluye como parte de la url.

Y también tendríamos que hacer una pequeña modificación en la vista para que se pase el parámetro como lo estamos esperando recibir.


<a th:href="@{eliminar?id=} + ${producto.id}" th:text="'Eliminar'" class="btn btn-outline-danger"></a>

Si optamos por pasar los parámetros en el cuerpo de la petición (POST, PUT, DELETE) como vemos en el método guardar no es necesario añadir ninguna anotación, simplemente tenemos que tener un parámetro que sea del tipo que esperamos recibir, de hecho ni siguiera es necesario que se llame igual y Spring

se encargará de rellenarlo con los datos que le lleguen de la vista.

Y por último y no por eso menos importante vamos a fijarnos en el return de nuestros métodos, en todos tenemos el nombre de la vista a la que tiene enviarnos el controlador después de procesar la llamada, pero en los métodos guardar y eliminar delante tienen un redirect:. El motivo de hacer el redirect es que necesitamos que antes de cargar la vista se pase por el controlador correspondiente para que se carguen los datos y no tengamos la vista vacía.

Si pruebas a quitar el redirect en guardar por ejemplo veras que aunque al pulsar en el botón guardar el producto se guardará correctamente la url va a seguir siendo http://localhost:9876/miapp/productos/guardar y la lista de productos se verá vacía porque el controlador en el que se rellena la lista de productos es en listar, pero para que se llame a ese método primero tiene que cambiar la url cosa que no sucede si no hacemos el redirect.

Si quieres probar el código tal y como estaba en el momento correspondiente a este post puedes hacerlo desde este enlace y si prefieres echarle un vistazo a como se encuentra en este momento o al repositorio en general pues puedes hacerlo aquí.

1 Compartir