Introducción a Spring Boot: API REST y Spring Data JPA

Última actualización: 25/10/2022
logo spring

Más que un marco de trabajo, Spring Framework es un enorme ecosistema de desarrollo que comprende decenas de módulos integrables entre sí. Se ha convertido, por méritos propios, en casi un estándar de facto en el mundo Java. Aúna potencia y sencillez, y siempre está a la vanguardia (Kotlin, microservicios, programación reactiva…).

Pero hablar de Spring es hacerlo también de Spring Boot.

En este tutorial veremos en qué consiste. Partiendo de cero, daremos los primeros pasos en el mundo de Spring de la mano de Spring Boot 2.7. El objetivo es crear una aplicación web Maven con una pequeña API REST para explotar una tabla de una base de datos MySQL.

Admito que el tutorial es más largo y denso de lo que me habría gustado; pero quiero ofrecerte un ejemplo real explicado con cierto detalle.

Con respecto al testeo de la aplicación (fundamental), he escrito otro tutorial específico que puede verse como la segunda parte del presente. De hecho, el proyecto de ejemplo es el mismo.

Tabla de contenidos

  1. ¿Qué es Spring Boot?
  2. Creando el proyecto con Spring Boot
    1. El pom
    2. Bitácora
    3. MySQL
  3. Spring Data JPA
  4. El fichero de configuración application.properties
  5. Inyección de dependencias
  6. Ejecutando el proyecto. La anotación @SpringBootApplication.
  7. Servicios REST
    1. Spring MVC en un minuto
    2. El primer servicio
    3. Ejecución de aplicación Web
      1. Autoejecutable
      2. War clásico
    4. Obtención de un país por su identificador
    5. Alta de país con @PostMapping. Validaciones automáticas.
    6. Actualización con @PutMapping
    7. Borrado con @DeleteMapping.
  8. Resumen final
  9. Código de ejemplo

¿Qué es Spring Boot?

En el tutorial Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache, expuse los pasos a seguir para configurar una aplicación Maven que integre Spring con JPA, utilizando Hibernate como implementación. No es difícil, pero resulta tedioso tener que replicar esta configuración cada vez que empecemos un proyecto nuevo.

La configuración puede complicarse si vamos a desarrollar un proyecto web. Necesitaremos también utilizar otros módulos de Spring tales como MVC, Security, etcétera. Muchas dependencias que a su vez precisan de otras, y todas deben funcionar en conjunto de manera armoniosa.

Numerosos programadores y empresas recurren a la creación de plantillas genéricas, por ejemplo en la forma de arquetipos Maven, que eviten tener que invertir tiempo en la creación y configuración de los nuevos proyectos desde cero. Esto conlleva el problema adicional de tener que mantenerlas actualizadas. Otras opciones son las plantillas de terceros, como las indexadas por yeoman, o irnos a marcos de trabajo basados en Spring, tales como JHipster o Grails.

Tampoco podemos olvidarnos de la mejor solución de todas: copiar un proyecto parecido y quedarnos con lo que necesitemos. Es broma, pero te aseguro que estas prácticas están a la orden del día…🤦

¡Spring Boot al rescate! Sin hacer mucho ruido, apareció allá por 2014 con la loable misión de simplificar y acelerar el desarrollo de aplicaciones basadas en Spring gracias a una gestión automatizada de la infraestructura software. En concreto:

«Spring Boot proporciona una manera rápida de construir aplicaciones. Inspecciona el classpath y los beans que tengas configurado, hace asunciones razonables sobre lo que te falta y lo añade. Puedes centrarte más en la lógica de negocio y menos en la infraestructura».

Para mí, la clave está en la última frase. Spring Boot disparará tu productividad porque te libera de la configuración e integración de las librerías de Spring, contemplando los casos más habituales. Ofrece clases y anotaciones bastante útiles. Incluso es capaz de incluir un servidor Tomcat o Jetty en tus aplicaciones web para que sean autoejecutables, algo muy útil en la era del cloud y los microservicios. Y lo mejor de todo es que no supone una limitación: podemos seguir realizando cualquier cosa posible con Spring.

Spring Boot es la mejor forma de trabajar con Spring. Espero que gracias a mis tutoriales consigas sacarle partido.

Creando el proyecto con Spring Boot

Contamos con varias herramientas para crear proyectos Spring Boot: el formulario web de Spring initializr, los asistentes de entornos de desarrollos tales como Eclipse (plugin Spring Tool Suite) e IntelliJ (solo la versión Ultimate de pago), y aplicaciones para la línea de comandos como Spring Boot CLI y SDKMAN (Linux y macOS).

Todas son estupendas, pero voy a crear y configurar el proyecto «a mano» para ir presentando los fundamentos de Spring Boot. Lo haré con Maven, aunque Spring Boot es compatible con Gradle.

Nota Aquí hago una breve presentación de Maven.

El pom

Como es habitual, empezamos configurando el pom.xml con las dependencias necesarias. Aquí viene la primera ventaja de adoptar Spring Boot: no definiremos como dependencias los módulos de Spring que necesitemos, sino una suerte de «metadependencias» llamadas starters (iniciadores) que incluyen todo lo necesario para sacar partido a una funcionalidad (seguridad, testeo, REST…). Adiós a los pom infinitos llenos de dependencias que muchas veces ni siquiera tenemos claro para qué sirven. Actualizas una y todo estalla en mil pedazos —hablo desde la experiencia—.

Añadimos lo siguiente al pom:

1-. Declarar Spring Boot como módulo padre.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.2</version>
    <relativePath />
</parent>

2-. La versión de Java la determina Spring Boot. La serie 2.x establece Java 8 y la 3.x, Java 17. Podemos cambiarla a una superior.

<properties>
    <java.version>11</java.version>
</properties>

3-. Añadir los iniciadores de las funcionalidades que necesitemos. Los disponibles se pueden consultar en GitHub en este enlace. Para Spring Data JPA con Hibernate es el siguiente.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

¡Todo lo necesario con una sola dependencia!

4-. El controlador o driver JDBC de la base de datos que vayamos a usar.

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <scope>runtime</scope>
</dependency>

5-. No es imprescindible, pero sí recomendable, añadir el plugin de Maven de Spring Boot. Nos ayudará a ejecutar y empaquetar nuestras aplicaciones. Otra funcionalidad interesante es la generación del fichero build-info.properties.

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

Llegados a este punto, el pom queda así. Lo modificaremos a medida que avance en el tutorial.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>com.danielme.demo</groupId>
    <artifactId>spring-boot</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
 
    <name>spring-boot-demo</name>
    <description>Demo project for Spring Boot</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.2</version>
        <relativePath />
    </parent>
 
    <properties>
        <java.version>11</java.version>
    </properties>
 
    <dependencies>
     
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>            
        </dependency>       
 
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
 
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
 
</project>

¿Dónde están las versiones? ¡No hacen falta declararlas! Spring Boot se encarga de ello. Por ejemplo, Spring Boot 2.7.2 usa Hibernate 5.6.10.Final. Olvídate de hacer juegos malabares para que todas las dependencias sean compatibles: lo garantiza Spring Boot.

Bitácora

El sistema de logging (registro de la bitácora) de Spring Boot se basa en la API genérica SLF4J que cuenta con implementaciones (adaptadores) para numerosas librerías de logging. Al usarla, podemos cambiar de librería sin modificar nuestro código. Todo esto lo explico aquí.

Spring Boot ofrece compatibilidad de serie con Log4j2, el sistema incluido en Java (java.util.logging) y Logback (opción predeterminada). Usaremos esta última. Su configuración se realiza en el fichero /src/main/resources/logback.xml. No obstante, contamos con la posibilidad de establecer configuraciones sencillas en el fichero de propiedades de la aplicación que veremos en breve.

Accedemos al sistema de logging con la interfaz org.slf4j.Logger.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
...
 
private static final Logger logger = LoggerFactory.getLogger(CountryController.class);

Si utilizas Lombok, dispones de la anotación @Slf4j.

MySQL

Para mayor comodidad, he incluido en la raíz del proyecto un fichero Dockerfile basado en la imagen oficial de MySQL 8. Al crearse un contenedor, se configura todo lo necesario porque se ejecutará el script init.sql

Con estas órdenes se construye la imagen y arranca un contenedor de usar y tirar (la opción –rm borrará el contenedor cuando se detenga).

docker build -t spring-boot-demo-mysql .
docker run --rm -d -p 3306:3306 spring-boot-demo-mysql

Docker 1: introducción
Docker 2: imágenes y contenedores

Obsérvese que el contenedor expone el puerto 3306 de MySQL hacia fuera a través del puerto 3306. Si tenemos más de un MySQL en ejecución, cada uno deberá estar accesible en un puerto distinto. Cuando te veas en esta situación, usa uno distinto al crear el contenedor y tenlo en cuenta al configurar la conexión.

La imagen de MySQL contiene las bases de datos country y country-test. Esta última será usada en el tutorial sobre testing, al igual que un pequeño script con datos de prueba (tres países) ubicado en /src/test/resources/test-mysql.sql. Estos datos ya se encuentran en la imagen.

Spring Data JPA

La explotación de una base de datos relacional en Java se realiza a través de la API genérica y estándar JDBC. Cuenta con proveedores para las distintas bases de datos del mercado (MySQL, PostgreSQL, Oracle…). Trabajar con ella es tedioso y consume mucho tiempo, no porque esté mal diseñada, sin por ser la API de más bajo nivel que tenemos para interactuar con bases de datos. Requiere escribir todas las operaciones con SQL, transformar datos, gestionar excepciones, transacciones, etcétera.

Jakarta Persistence, más conocido por las siglas JPA, aligera esta enorme carga de trabajo. Es un estándar englobado en Jakarta EE que nos permite trabajar de forma más abstracta y cómoda con las bases de datos relacionales. Tiene anotaciones con las que configuramos la relación entre las tablas de la base de datos y clases Java que las representan, llamadas entidades. Basándose en estas entidades, los productos compatibles con JPA, como Hibernate, deben ser capaces de sincronizar los datos entre las entidades y las tablas, realizando actualizaciones, consultas, borrados, etcétera, sin que tengamos que escribir SQL. Todo esto entre muchas otras funcionalidades.

Vamos a añadir una entidad JPA que representa a una tabla llamada countries. Tenemos un identificador \ clave primaria de tipo identidad, esto es, generado por la base de datos, y dos atributos que no pueden ser nulos, uno de ellos de valor único para todos los países.

@Entity
@Table(name = "countries")
public class Country {
     
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(nullable = false, unique = true)
    private String name;
 
    @Column(nullable = false)
    private Integer population;

La interacción con JPA se realiza con el gestor de entidades. Pero en Spring, podemos ir más allá y añadir otra capa de abstracción con Spring Data JPA. En interfaces llamadas repositorios que especialicen a Repository, declararemos métodos que busquen entidades, las actualicen, eliminen, ejecuten SQL… Y cuando las capacidades automáticas del repositorio no sean suficientes, podemos implementar con código todo lo que necesitemos. Al igual que Spring Boot, Spring Data facilita nuestro trabajo sin interponerse en nuestro camino.

Spring Data JPA Rocks

En el proyecto de ejemplo, lo anterior se materializa en una interfaz que especializa a JpaRepository, tipada para la entidad Country y su identificador. Es suficiente con los métodos que proporciona JpaRepository.

package com.danielme.springboot.repositories;
 
import org.springframework.data.jpa.repository.JpaRepository;
 
import com.danielme.springboot.entities.Country;
 
public interface CountryRepository extends JpaRepository<Country, Long>{
 
}

Para más información, consulta los más de 20 capítulos que dedico a JPA e Hibernate en mi curso Jakarta EE y el minicurso de Spring Data JPA.

El fichero de configuración application.properties

Ahora pasamos a definir los parámetros de configuración de los distintos módulos de Spring y otras librerías que Spring Boot incorpora al proyecto. Lo haremos en el fichero /src/main/resources/application.properties o /src/main/resources/application.yaml (podemos usar los dos al mismo tiempo). En esta página encontramos una lista con las propiedades más habituales. También podemos incluir propiedades de configuración personalizadas, tal y como explico en Tips Spring : [BOOT] propiedades de configuración personalizadas.

De momento, es suficiente con una configuración mínima que permita interactuar con una base de datos MySQL mediante JPA e Hibernate.

spring.datasource.url=jdbc:mysql://localhost:3306/country
spring.datasource.username=demo
spring.datasource.password=demo
spring.jpa.hibernate.ddl-auto=update

Para MySQL, proporcionamos la url de conexión y las credenciales de un usuario. En el caso de Hibernate, establecemos también el modo DDL (*). Indica a Hibernate qué debe hacer cuando arranque en lo que respecta a la coherencia del modelo de entidades con las tablas. Por omisión, no hará nada (valor none). Con el valor update, Hibernate será capaz de modificar la base de datos para que sea coherente con la configuración de las entidades. ¡No hagas esto en producción si no tienes claro lo que estás haciendo!

(*) Este acrónimo significa «lenguaje de definición de datos» y se refiere a las órdenes de SQL que sirven para crear y cambiar la estructura de la base de datos (CREATE, ALTER, DROP…).

No se precisa nada más para echar a andar la primera versión del proyecto. Todo se configura automáticamente, incluyendo el dialecto de Hibernate. Podemos forzar uno concreto con la propiedad spring.jpa.database-platform.

Inyección de dependencias

Añadimos una clase de servicio para implementar los procesos de negocio relativos a los países. Más adelante se incorporarán nuevos métodos.

package com.danielme.springboot.services;
 
import java.util.List;
import java.util.Optional;
 
import org.springframework.stereotype.Service;
 
import com.danielme.springboot.entities.Country;
import com.danielme.springboot.repositories.CountryRepository;
 
@Service
public class CountryService {
 
    private final CountryRepository countryRepository;
 
    public CountryService(CountryRepository countryRepository) {
        this.countryRepository = countryRepository;
    }
 
    public List<Country> findAll() {
        return countryRepository.findAll();
    }
     
    public Optional<Country> findById(Long id) {
        return countryRepository.findById(id);
    }
 
}

Con la anotación @Service estamos indicando que CountryService es una clase de tipo servicio que será gestionada por el sistema de inyección de dependencias de Spring. Esto significa que Spring creará un objeto único de esta clase (un singleton, comportamiento predeterminado) que proporcionará (inyectará) a todos los objetos de las clases que así lo soliciten.

La condición es que esas clases también deben ser parte del sistema de inyección de dependencias. Este comportamiento es recíproco: todas las clases gestionadas por Spring pueden ser inyectadas unas en otras, siempre y cuando no se formen relaciones circulares. Entre estas clases se encuentran: los repositorios de Spring Data; las marcadas con estereotipos (la anotación @Component y las que la incluyan como @Service, @Controller o @Repository; y aquellas cuyos objetos sean construidos en métodos factoría anotados con @Bean en clases marcadas con @Configuration.

CountryService tiene como dependencia la interfaz CountryRepository, así que he declarado el atributo correspondiente. ¿Cómo sabe Spring que debe rellenarlo? A fin de cuentas, podemos tener atributos que no sean dependencias. La inyección de una dependencia se indica con la anotación @Autowired.

@Autowired
private CountryRepository countryRepository;

Sin embargo, en CountryService no hay rastro de ella. Esto es porque he aprovechado una convención de Spring: dado que CountryService dispone de un único constructor, Spring no tendrá más remedio que usarlo para instanciar la clase y asume que todos sus parámetros son dependencias a inyectar.

Ejecutando el proyecto. La anotación @SpringBootApplication.

Antes de dar el salto al mundo web, veamos cómo ejecutar el proyecto. El punto de entrada de un proyecto Spring Boot es un método main estándar. Estará en una clase anotada con @SpringBootApplication. Se trata de una «meta-anotación» que aplica las funcionalidades de las tres siguientes.

  • @Configuration. Permite que la clase defina configuraciones de Spring.
  • @EnableAutoConfiguration. Activa las configuraciones que Spring Boot es capaz de realizar de manera automática en función de los starters del proyecto.
  • @ComponentScan. Activa la búsqueda y empleo automático de beans de Spring, como las clases marcadas con los estereotipos @Component, @Controller, @Service o @Repository.

Dentro del método main arrancamos Spring invocando al método estático SpringApplication#run. Sus argumentos son una clase de configuración inicial (lo mejor es usar la clase @SpringBootApplication) y los parámetros pasados a Java por línea de comandos al main.

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

Todo esto está muy bien, ¡pero la aplicación no hace nada interesante! Mostremos en la bitácora los países de la tabla countries usando CountryService.

Para ejecutar código en el arranque después del inicio de Spring, implementaremos la interfaz funcional CommandLineRunner. Lo más cómodo es hacerlo en CountriesApp.

@SpringBootApplication
public class CountriesApp implements CommandLineRunner {
 
    private static final Logger logger = LoggerFactory.getLogger(CountriesApp.class);
 
    @Autowired
    CountryService countryService;
 
    public static void main(String[] args) {
        SpringApplication.run(CountriesApp.class, args);
    }
 
    @Override
    public void run(String... arg0) {
        countryService
                .findAll()
                .forEach(c -> logger.info(c.toString()));
    }
 
}

Aseguramos que los parámetros de conexión a MySQL están bien configurados, en especial el puerto, y que existan tanto la base de datos y el usuario con los permisos adecuados. La tabla countries, si no existe, se creará la primera vez que se ejecute la aplicación porque dimos a la propiedad spring.jpa.hibernate.ddl-auto el valor update.

Así ha quedado la estructura del proyecto.

Ya estamos listos para ejecutar el método main desde nuestro IDE favorito y comprobar la bitácora. Veremos en la terminal donde ejecutemos el proyecto el logotipo de Spring (se puede personalizar), mensajes varios y los registros de countries, acción realizada por CountriesApp#run.

07-31-13:59:35.872 [restartedMain] INFO  com.danielme.springboot.CountriesApp - Started CountriesApp in 2.407 seconds (JVM running for 2.95)
07-31-13:59:36.016 [restartedMain] INFO  com.danielme.springboot.CountriesApp - Country{id=1, name='Mexico', population=130497248}
07-31-13:59:36.016 [restartedMain] INFO  com.danielme.springboot.CountriesApp - Country{id=2, name='Spain', population=49067981}
07-31-13:59:36.016 [restartedMain] INFO  com.danielme.springboot.CountriesApp - Country{id=3, name='Colombia', population=46070146}

Otra opción es sacar partido del plugin de Maven de Spring Boot para que empaquete toda la aplicación en un fichero jar ejecutable.

mvn package spring-boot:repackage

Este comando genera dos ficheros .jar en la carpeta target.

spring-boot:repackage

El pequeño contiene solo las clases propias del proyecto. El grande incluye todas las librerías y puede ser ejecutado con java -jar.

Si queremos ejecutar la aplicación «al vuelo» sin empaquetarla, es más rápido ejecutar

mvn spring-boot:run

Servicios REST

Vamos a desarrollar los servicios REST que cubren el juego de operaciones CRUD para los países: consulta, creación, actualización y borrado. Todo lo que veremos son características de Spring MVC, por lo que nos servirán aunque no usemos Spring Boot.

Esta es la API completa mostrada en la vista Endpoints de IntelliJ Ultimate.

Spring MVC en un minuto

Spring MVC es responsable de las capacidades web de los proyectos desarrollados con Spring. Las conseguimos con este iniciador:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>   
</dependency>

Esta ilustración muestra el flujo simplificado de trabajo que sigue una petición HTTP.

DispatcherServlet es el corazón de Spring MVC. Recibe las peticiones HTTP y las envía al controlador responsable de procesar la url solicitada. Los controladores son las clases que nosotros escribimos para gestionar las peticiones HTTP entrantes.

A continuación, DispatcherServlet recibe la respuesta del controlador e invoca a un componente llamado ViewResolver. Su cometido es renderizar la vista adecuada: una página web. Puede ser estática (el contenido de un fichero tal cual) o bien dinámica (generada con tecnologías como Thymeleaf o Jakarta Server Pages). DispatcherServlet devolverá la vista en la respuesta HTTP que recibirá el solicitante de la petición.

Interesante, ¡pero nosotros no queremos mostrar páginas web! Tenemos una API REST que retorna documentos JSON. Esto es una menudencia para Spring; en breves instantes veremos cómo informar de este hecho a DispatcherServlet.

El primer servicio

¿Cómo materializamos la anterior explicación en un endpoint de una API REST? Creando una clase controladora. Esto es, una anotada con @Controller.

@Controller
@RequestMapping(CountryRestController.COUNTRY_RESOURCE)
public class CountryRestController {
     
    public static final String COUNTRIES_RESOURCE = "/api/countries";
 
    private final CountryService countryService;
 
    public CountryRestController(CountryService countryService) {
        this.countryService = countryService;
    }

La anotación @RequestMapping es opcional. Establece una url raíz común a todos los endpoint REST declarados en la clase.

Veamos el primer método de CountryRestController.

@GetMapping
@ResponseBody
public List<Country> getAll() {
    return countryService.findAll();
}

Magnífico. Para obtener la lista de países vía REST tan solo se precisa un método que la devuelva marcado con dos anotaciones:

  • Una que indique el verbo HTTP (GET, POST, PUT…) al que atiende. En el ejemplo, se trata de @GetMapping. La anotación puede incluir la url relativa que procesa el método y que se añade a la que hayamos definido con @RequestMapping en la clase. Dado que no hemos establecido una url en @GetMapping, el método getAll implementa el servicio GET /api/countries.
  • @ResponseBody indica que el retorno del método es el cuerpo de la respuesta HTTP. La consecuencia es que nuestro endpoint devolverá un documento JSON (de ello se encargará Spring) y no una vista generada por ViewResolver.

Lo habitual -y lógico- es que las todas las urls gestionadas por los métodos de un controlador devuelvan páginas web o bien sean endpoints de una API REST. Por ello, en este último caso lo más cómodo es marcar la clase con @ResponseBody para que la anotación se aplique a todos los métodos. Y mejor todavía recurrir a @RestController, una anotación de conveniencia que incluye @Controller y @ResponseBody. Usémosla para hacer el código más obvio: con ver @RestController queda claro el cometido de la clase y sus métodos.

@RestController
@RequestMapping(CountryRestController.COUNTRY_RESOURCE)
public class CountryRestController {
 
@GetMapping
public List<Country> getAll() {
    return countryService.findAll();
}

Antes de proseguir, una advertencia: no es apropiado devolver en los servicios entidades JPA. En primer lugar, porque lo habitual es que no se corresponden con la estructura de datos que queremos enviar. En segundo lugar, y esto es una cuestión de diseño y arquitectura, porque estamos exponiendo la estructura de la base de datos y usando en las interfaces de comunicación con la aplicación clases que pertenecen al módulo de persistencia. Esto implica que cambios internos del sistema pueden afectar a los clientes externos. En cualquier caso, para nuestro ejemplo nos sirve.

Ejecución de aplicación Web
Autoejecutable

¿Cómo ejecutamos un proyecto Spring Boot de tipo web? Pues como ya sabemos: con su main. La aplicación sigue siendo autoejecutable porque se empaqueta junto a un servidor Apache Tomcat. De nuevo, la magia de Spring Boot en acción.

Ok, pero ¿qué es Tomcat? ¿Por qué lo necesitamos?

Las aplicaciones web Java se basan en el uso de servlets: las clases capaces de procesar peticiones HTTP. Aunque en la mayoría de proyectos Spring no necesitaremos escribir servlets, siempre están ahí, en la sombra, haciendo el trabajo sucio. Fíjate que DispatcherServlet es un servlet.

Los servlets son ejecutados por un «contenedor de servlets». Por tanto, las aplicaciones web Java se deben ejecutar dentro de uno de esos contenedores. El más popular es Tomcat, hasta el punto de ser un estándar de facto. Ofrece muchos otros servicios, incluyendo un completo servidor web con soporte https. No necesitamos nada más, como un servidor web Nginx o similar.

El Tomcat con el que Spring Boot ejecuta el proyecto atiende al puerto 8080 y sitúa las urls que hayamos implementado a partir de la raíz. Podemos cambiar esta configuración con estos parámetros:

server.servlet.context-path=/spring-boot-demo
server.port=9090

He dejado la configuración predeterminada en el proyecto de ejemplo, por lo que la aplicación se encuentra en http://localhost:8080/. Llamaremos al servicio REST añadiendo la url definida en la anotación @RequestMapping, así que la dirección completa que nos proporciona la lista de países es http://localhost:8080/api/countries. Al ser una petición GET, podemos realizarla con cualquier navegador.

[
    {      
        "id": 1,
        "name": "Mexico",
        "population": 130497248
    }
]

Nota: para trabajar con APIs REST, aconsejo herramientas gráficas gratuitas como Insomnia o Postman.

List<Country> se ha transformado en un JSON gracias a que Spring Boot añade al proyecto la librería Jackson. Exporta los valores, incluyendo nulos, de todos los getters de la clase, en nuestro caso Country. Jackson es muy potente y configurable; se puede personalizar, cómo no, en el application.properties.

War clásico

No estamos obligados a utilizar la característica de autoejecución de Spring Boot. Podemos construir nuestra aplicación de forma tradicional creando un fichero de tipo .war ejecutable por un contenedor de servlets. Tan sencillo como:

1-. Hacer que la clase @SpringBootApplication herede de SpringBootServletInitializer.

@SpringBootApplication
public class CountriesApp extends SpringBootServletInitializer
{

2-.Cambiar en el pom el tipo de empaquetado a war.

<packaging>war</packaging>

3-. Añadir el iniciador de Tomcat con ámbito provided porque no queremos que el war incluya sus librerías. No las necesitamos y podrían causar problemas en el classpath cuando ejecutemos la aplicación.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

Con estos cambios, Maven crea dos artefactos de tipo .war. Tomaremos el pequeño porque no contiene las librerias de Tomcat y de autoejecución. De esto se deduce que solo el más grande es autoejecutable.

Para ejecutar la aplicación en Tomcat, se debe colocar su archivo .war en la carpeta /{tomcat}/webapps y arrancar Tomcat. Esta acción se conoce como «despliegue» o deploy.

Obtención de un país por su identificador

El segundo servicio devuelve un país concreto.

@GetMapping(value = "/{id}")
public Country getById(@PathVariable("id") Long id) {
    return countryService.findById(id).orElse(null);
}

El identificador del país forma parte de la url, así que este servicio requiere que se defina su url relativa en la anotación @GetMapping, teniendo en cuenta que la url completa se obtendrá añadiéndola a la dirección indicada en @RequestMapping. Por ser un parámetro variable incluido en la url, le damos un nombre contenido entre llaves. Lo recibimos en el método declarando un argumento anotado con @PathVariable. Si el nombre de ese argumento coincide con el empleado en la url, no hace falta indicarlo en la anotación.

Si admitiéramos parámetros establecidos en la parte de consulta de la url (?name=Spain), bastaría con declararlos en el método con la anotación @RequestParam. De nuevo, si el nombre del argumento coincide con el esperado no hay que indicarlo.

Todas las variables definidas con @PathVariable y @RequestParam son obligatorias. Si no se reciben, se lanzarán, de forma respectiva, las excepciones MissingPathVariableException y MissingServletRequestParameterException. Las configuramos como opcionales estableciendo la propiedad required de las anotaciones a false o bien declarándolas de tipo Optional. En el caso de @RequestParam, se puede definir un valor por omisión con defaultValue.

@GetMapping
public List<Country> search(@RequestParam(required=false) String name) {

El método anterior es equivalente al siguiente.

@GetMapping("/search")
public List<Country> search(@RequestParam Optional<String> name) {

Volvamos al método getById. Obsérvese que si el país solicitado no existe, devuelve null. Esto no supone un problema, pero si queremos diseñar una buena API REST, en este caso lo mejor es devolver el código HTTP 404, esto es, no encontrado. De forma predeterminada, los servicios REST de Spring devuelven OK (200).

La clase ResponseEntity modela la respuesta HTTP completa. Permite construir con todo detalle la respuesta de los servicios (header, código de estado, body). Está tipada para la clase devuelta en el body.

@GetMapping(value = "/{id}")
public ResponseEntity<Country> getById(@PathVariable("id") Long id) {
    return countryService
            .findById(id)
            .map(value -> new ResponseEntity<>(value, HttpStatus.OK))
            .orElseGet(() -> new ResponseEntity<>(null, HttpStatus.NOT_FOUND));
}

Mejor ahora. No obstante, el código queda más claro si construimos la respuesta con los métodos estáticos que contiene ResponseEntity y que se basan en clases internas de tipo Builder (HeadersBuilder y BodyBuilder).

@GetMapping(value = "/{id}")
public ResponseEntity<Country> getById(@PathVariable("id") Long id) {
    return countryService
            .findById(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
}
Alta de país con @PostMapping. Validaciones automáticas.

Es el turno del alta de un nuevo país. Es un servicio de tipo POST que espera recibir un JSON con el nombre y la población.

{
    "name": "Germany",
    "population": 79778000
}

Resulta tentador reutilizar para la estructura de datos anterior la entidad Country. Es una mala idea, pues cuenta con propiedades tales como id que nunca debería proporcionar el solicitante. Seamos diligentes y definamos una nueva clase. Asimismo, con la API estándar de Bean Validation imponemos restricciones a validar.

public class CountryRequest {
 
    @NotEmpty
    private String name;
 
    @NotNull
    @Min(1)
    @Max(2000000000)
    private Integer population;

Se requiere esta dependencia.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Así queda el nuevo método.

@PostMapping
public ResponseEntity<Map<String, Object>> add(@RequestBody @Valid CountryRequest country) {
    Long id = countryService.create(country);
    return new ResponseEntity<>(Collections.singletonMap("id", id), HttpStatus.CREATED);
}

En esta ocasión, utilizamos la anotación @PostMapping para indicar que el servicio espera una llamada de tipo POST. Los datos del país, un JSON en el cuerpo de la petición HTTP, se reciben en un objeto CountryRequest añadiéndolo como un argumento anotado con @RequestBody. Al igual que @PathVariable y @RequestParam, esta anotación cuenta con la propiedad required (por omisión es true) para establecer la obligatoriedad de la existencia del argumento.

He acompañado la declaración del argumento con @Valid para que se apliquen las validaciones declaradas en los atributos de CountryRequest. Si se incumplen, se devuelve el código HTTP 400 (petición errónea) y un JSON detallando los problemas.

Esta es la respuesta si el cliente del servicio olvidó enviar el nombre del país.

"timestamp": "2022-10-25T19:06:15.646+00:00",
    "status": 400,
    "error": "Bad Request",
    "trace": "org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public ...
    "message": "Validation failed for object='countryRequest'. Error count: 1",
    "errors": [
        {
            "codes": [
                "NotEmpty.countryRequest.name",
                "NotEmpty.name",
                "NotEmpty.java.lang.String",
                "NotEmpty"
            ],
            "arguments": [
                {
                    "codes": [
                        "countryRequest.name",
                        "name"
                    ],
                    "arguments": null,
                    "defaultMessage": "name",
                    "code": "name"
                }
            ],
            "defaultMessage": "must not be empty",
            "objectName": "countryRequest",
            "field": "name",
            "rejectedValue": null,
            "bindingFailure": false,
            "code": "NotEmpty"
        }
    ],
    "path": "/api/countries"
}

Demasiada información. Aquí explico cómo personalizar el JSON con los errores de validación.

En caso de que todo vaya bien y la inserción sea correcta, retornaremos el identificador del nuevo país en un JSON con el atributo id. El código apropiado para estos casos es 201 («creado»).

Dos posibles mejoras.

  • Deberíamos modelar la respuesta con una clase, pero he aprovechado para mostrar cómo construir un JSON de forma dinámica con un Map.
  • Si se intenta crear un país que ya existe (no se admiten nombres duplicados), Hibernate lanzará una excepción y el servicio responderá con un estado 500 (error interno). Es más elegante y educado validar que el país no existe, y cuando exista informar de ello con un código 400.
Actualización con @PutMapping

La actualización se hará con una llamada PUT a la url del país. En caso de éxito no se devuelve nada, por lo que la clase de la respuesta es Void y el código HTTP será 204: operación exitosa sin contenido en el body de la respuesta. Pero cuando el país a actualizar no exista, devolvemos un 404.

@PutMapping(value = "/{id}")
public ResponseEntity<Void> update(@PathVariable Long id,
                                   @RequestBody @Valid CountryRequest countryRequest) {
    boolean wasUpdated = countryService.update(id, countryRequest);
    return wasUpdated ? ResponseEntity.noContent().build() : ResponseEntity.notFound().build();
}

El método que he añadido al servicio es muy interesante.

@Transactional
public boolean update(Long id, CountryRequest countryRequest) {
  return countryRepository.findById(id)
            .map(country -> {
                copy(countryRequest, country);
                return true;
            })
            .orElse(false);
}
 
private void copy(CountryRequest countryRequest, Country country) {
  country.setName(countryRequest.getName());
  country.setPopulation(countryRequest.getPopulation());
}

En lugar de un boolean que indique la existencia del país, sería preferible lanzar una excepción creada por nosotros para estos casos, habituales cuando la API vaya creciendo con nuevas funcionalidades. Mejor todavía si verificamos la validez de los datos que entran en nuestro sistema con un sistema de validaciones como el que Spring tiene. Hablo acerca de ambas estrategias aquí.

Con todo, el punto de mayor interés está en la manera en la que se actualiza el país. Obtenemos la entidad con el repositorio y la modificamos, pero ¡no la guardamos! (hay métodos save en los repositorios). La clave está en que el método es transaccional: cuando estamos dentro de una transacción, Hibernate es capaz de sincronizar automáticamente las entidades presentes en ella con la base de datos. Detecta que la entidad country se modificó y lanzará una sentencia SQL de UPDATE. Este funcionamiento lo explico con detalle aquí.

Borrado con @DeleteMapping.

Concluyo el tutorial con la operación de borrado que, en realidad, asegura que el país indicado no existe en el sistema. Por ello, si nos solicitan eliminar un país inexistente en la base de datos, no se devuelve un 404: la operación es correcta.

@DeleteMapping(value = "/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
    countryService.delete(id);
}

@ResponseStatus establece el código de la respuesta. En este caso concreto, nos ahorramos crear un ResponseEntity. En ejemplos anteriores, esta solución es no sirve porque el código de respuesta es variable.

Resumen final

Fue un tutorial extenso, y aun así apenas hemos arañado la superficie de Spring Boot. Resumo y ordeno los pasos que nos llevaron a crear la API REST que explota una base de datos relacional.

  1. En el pom heredamos del pom padre de Spring Boot y añadimos los iniciadores para los módulos de Spring necesarios (web, validation y Spring Data JPA). No puede faltar el controlador JDBC de la base de datos que usemos.
  2. El fichero application.properties contiene los parámetros de configuración de Spring Boot y de las librerías compatibles. Necesitamos indicar, como mínimo, los datos de conexión a la base de datos.
  3. Modelamos en entidades de JPA las tablas de la base de datos y las relaciones entre ellas.
  4. Creamos los repositorios de Spring Data JPA, vinculados a las entidades, con las operaciones a realizar con la base de datos.
  5. La lógica de negocio \ casos de uso se implementan en una clase de tipo «servicio» o similar. En ella «inyectamos» los repositorios.
  6. La API REST se implementa en clases controladoras (@RestController) que, a su vez, delegan el trabajo en las clases de servicio. En los controladores tendremos un método para cada url (endpoint de la API) marcado con @GetMapping, @PostMapping…Estos métodos pueden recibir el cuerpo de la petición (@RequestBody), parámetros situados dentro de la url (@PathVariable) y \o variables de tipo query añadidas a la url (@RequestParam). Devuelven los objetos tal cual, o contenido en un ResponseEntity. De su conversión a JSON ya se encargará Spring gracias a Jackson.
  7. Las aplicaciones Spring Boot se arrancan mediante un método main estándar situado en una clase anotada con @SpringBootApplication. Incluyen un Tomcat embebido en el que se despliega la aplicación.

Código de ejemplo

El proyecto para el presente tutorial y el de testing se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Otros tutoriales relacionados

Spring Security \ Spring Boot: Segurización API REST con BASIC y base de datos (SQL y Spring Data). Testing.

Spring y Spring Boot: Gestión de errores en aplicaciones web y REST

Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos en memoria.

Spring BOOT: integración con JSP

Spring JDBC Template: simplificando el uso de SQL

Persistencia en BD con Spring Data JPA

4 comentarios sobre “Introducción a Spring Boot: API REST y Spring Data JPA

  1. Buenas daniel, excelente artículo. Aunque no he entendido como has llegado a introducir datos en la base de datos antes de ejecutarla.
    Un saludo.

    1. Los datos de ejemplo que uso en el tutorial están en el script /src/test/resources/test-mysql.sql. He actualizado el artículo para aclararlo, y también he mejorado la imagen de Docker para que ya incluya esos datos.

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.