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

Última actualización: 4/03/2023
logo spring

Más que un marco de trabajo, Spring Framework es un enorme y vibrante 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 con Spring de la mano de Spring Boot 3.0, la última versión estable en marzo de 2023. El objetivo es crear una aplicación web Maven con una pequeña API REST que explota 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 considerarse como la segunda parte del presente. De hecho, el proyecto de ejemplo es el mismo.

Casi todos los conceptos que veremos se tratan con gran detalle en muchos otras publicaciones del blog que iré enlazando.

Índice

  1. ¿Qué es Spring Boot?
  2. Creando el proyecto con Spring Boot
    1. El pom
    2. MySQL
  3. JPA
  4. Entidades
  5. Spring Data JPA
  6. El fichero de configuración application.properties
  7. Bitácora (sistema de logs)
  8. Inyección de dependencias
  9. Ejecutando el proyecto. La anotación @SpringBootApplication.
  10. 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
  11. Resumen final
  12. Código de ejemplo

¿Qué es Spring Boot?

En el tutorial Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache, describo 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 replicar y adaptar esta configuración cada vez que empecemos un proyecto nuevo.

La configuración puede complicarse si vamos a desarrollar un proyecto web. Necesitaremos 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 invertir tiempo en la creación y configuración de los nuevos proyectos desde cero. Esto conlleva el problema adicional de mantenerlas actualizadas. Otras opciones son las plantillas de terceros, como las indexadas por yeoman, o irnos a marcos de trabajo basados en Spring (JHipster o Grails).

Tampoco podemos olvidarnos de la mejor solución: 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, 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 habituales. Asimismo, ofrece clases y anotaciones bastante prácticas. 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. A todo esto hay que añadir que podemos seguir realizando cualquier tarea posible con Spring.

En definitiva, Spring Boot es la mejor forma de trabajar con Spring. Espero que con la ayuda de mis humildes publicaciones 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); aplicaciones para la línea de comandos (Spring Boot CLI y SDKMAN).

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 también es compatible con Gradle.

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

El pom

Comencemos 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 poms infinitos llenos de dependencias que muchas veces ni siquiera sabemos para qué sirven. Actualizas una y todo estalla en mil pedazos.

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>3.0.4</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. Por ende, el proyecto requiere Java 17 como mínimo. Puedes configurar una versión superior así:

<properties>
    <java.version>19</java.version>
</properties>

3-. Agregar los iniciadores. 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. MySQL es un tanto particular, pues la dependencia del controlador depende de la versión de Spring Boot.

Spring Boot 3:

<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

Spring Boot 2:

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

En verdad, ambas son la misma dependencia. Oracle cambió su nombre a finales de 2022, tal como se explica aquí.

5-. Si bien no es imprescindible, añadamos el plugin de Maven de Spring Boot. Como pronto descubrirás, nos ayudará a ejecutar y empaquetar los proyectos. Otra funcionalidad 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>3.0.3</version>
        <relativePath />
    </parent> 
 
    <dependencies>
     
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>            
        </dependency>       
 
        <!--Spring Boot 3 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!--Spring Boot 2 -->
        <!--<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </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 hace falta declararlas! Spring Boot se encarga de ello. Por ejemplo, en Spring Boot 3.0.3 tendremos Hibernate 6.1.7.Final. Olvídate de practicar juegos malabares para conseguir que las dependencias sean compatibles: lo garantiza Spring Boot.

Puedes importar el proyecto en cualquier entorno de desarrollo (IDE) compatible con Maven, como los celebérrimos Eclipse e IntelliJ. En este artículo te explico cómo hacerlo con los proyectos del blog.

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 servidor MySQL en ejecución, cada uno deberá atender en un puerto diferente. Cuando te veas en esta situación, publica 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 script con datos de prueba (tres países) ubicado en /src/test/resources/test-mysql.sql.

JPA

La explotación de una base de datos relacional se realiza a través de la API genérica y estándar JDBC. Disponemos de implementaciones de ella —los controladores JDBC que mencioné— para las distintas bases de datos del mercado (MySQL, PostgreSQL, Oracle…).

Programar con JDBC es fastidioso y consume mucho tiempo, no porque esté mal diseñada, sin por ser la API de nivel más bajo que tenemos para interactuar con bases de datos. Se 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 abstracta y cómoda con las bases de datos relacionales. Provee anotaciones con las que configuramos la equivalencia entre las tablas de la base de datos y ciertas clases especiales. Los objetos de esas clases se denominan entidades y representan a los registros de las tablas.

Los productos compatibles con JPA, como Hibernate, deben ser capaces de sincronizar las entidades y las tablas, realizando actualizaciones, consultas, borrados, etcétera, sin que tengamos que escribir SQL. Todo esto entre muchas otras funcionalidades.

Para más información, consulta los más de veinte capítulos que dedico a JPA e Hibernate en mi curso Jakarta EE.

Nota sobre versiones. A partir de JPA 3.0, los paquetes que empezaban por javax lo hacen con jakarta. Spring Boot dio el salto a JPA 3.1 en la versión 3.0. Tenlo presente cuando trabajes con Spring Boot 2 (usa JPA 2.2). Aquí expongo la razón de este nuevo nombre.

Entidades

Esta clase representa a una tabla llamada countries:

package com.danielme.springboot.entities;

import jakarta.persistence.*;

@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;

Country es una clase de tipo entidad porque está marcada con @Entity. La anotación @Table (opcional) establece el nombre de la tabla que representa. Por omisión, se asume que el nombre de la tabla coincide con el de la clase.

Como mínimo, toda clase de tipo entidad debe contar con el constructor vacío y un campo marcado con @Id (el identificador) que se corresponda con la clave primaria de la tabla. En nuestro caso, será un número generado por la base de datos (el generador de tipo identidad). Por tanto, el atributo id distingue unívocamente a las entidades del mismo tipo.

También contamos con dos atributos que representan las columnas de la tabla que almacenan el nombre del país y su población. @Column indica que no pueden ser nulos (nullable=»false») y que el valor de name es único para todos los registros de la tabla (unique=true).

JPA asume que todos los atributos de la clase se corresponden con columnas de igual nombre. Puedes indicar el nombre de la columna con la propiedad name de @Column.

Te dejo una representación visual para que veas la relación entre la clase y la tabla.

Spring Data JPA

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 llamada 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 lo que queramos. Al igual que Spring Boot, Spring Data nos facilita la vida 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 y que está tipada para la clase Country y su identificador. Será 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>{
 
}

No merece la pena entrar en más detalles; para eso ya está mi curso 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 parámetros de configuración personalizados, asunto tratado 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 ese modelo. ¡No hagas esto en producción si no tienes claro lo que estás haciendo!

(*) Acrónimo que significa «lenguaje de definición de datos». 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 JPA con Hibernate.

Bitácora (sistema de logs)

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 el 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.

Accedemos al sistema de logging con el fin de escribir mensajes mediante 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.

La configuración de Logback se declara 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. Por lo común, querrás establecer el nivel general en INFO:

logging.level.root=INFO

Y luego ir ajustando los datos que quieras ver con detalle. Te dejo algunas configuraciones útiles para revisar el funcionamiento del proyecto de ejemplo:

#sentencias SQL que Hibernate ejecuta
logging.level.sql=debug

#parámetros consultas SQL en Hibernate 5 \ Spring Boot 2
#logging.level.org.hibernate.type=trace

#parámetros consultas SQL en Hibernate 6 \ Spring Boot 3
logging.level.org.hibernate.orm.jdbc.bind=trace

#entidades que lee Hibernate
logging.level.org.hibernate.orm.results.loading.entity=trace

#la parte web de Spring
logging.level.org.springframework.web.servlet=trace
spring.mvc.log-request-details=true

Inyección de dependencias

El patrón de diseño llamado inyección de dependencias es la piedra angular que sostiene al ecosistema de desarrollo de Spring, dotándolo de una enorme modularidad y flexibilidad. En esta sección hago una presentación somera de este patrón centrada en su empleo en Spring; aquí tienes una explicación más detallada y general de ese patrón

Añadamos una clase de servicio para implementar los procesos de negocio relativos a los países. Se apoya en CountryRepository:

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);
    }
 
}

@Service indica 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. Se dice que esas clases tienen a CountryService como dependencia porque su código depende de ella (la necesitan). Nótese que por ser este singleton reutilizable, no puede tener estados; es decir, la clase carece de atributos que cambien de valor.

La condición para que todo funcione es que esas clases que dependen de CountryService también formen 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. Entre estas clases se encuentran: los repositorios-interfaces 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.

Si ahora vuelves al código de CountryService, deducirás que tiene como dependencia la interfaz CountryRepository la cual, al ser un repositorio, puede recibir de Spring. Pero ¿cómo sabe Spring que debe inyectar el objeto singleton de CountryRepository en el campo countryRepository? Pues bien, la inyección de una dependencia se indica marcando con @Autowired el atributo que la recoge:

@Autowired
private CountryRepository countryRepository;

O bien, menos habitual, su setter:

@Autowired
public void setCountryRepository(CountryRepository countryRepository) {
    this.countryRepository = countryRepository;
}

Sin embargo, en CountryService no hay rastro de ella. Esta omisión se debe a que 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 asumiendo que todos sus parámetros son dependencias a inyectar. Si hubiera más de un constructor, ahí sí que tendremos que marcar uno de ellos, y solo uno, con @Autowired.

La inyección en constructor se prefiere:

  1. Permite asegurar que el objeto es inmutable haciendo finales sus atributos. La mutabilidad de los objetos constituye una gran fuente de problemas a la que no se suele prestar la debida atención.
  2. Con los constructores facilitamos que el objeto se cree de forma correcta proporcionándoles las dependencias que nos exijan.

Quédate con la expresión «bean de Spring»: es el nombre genérico que reciben los objetos gestionados por el sistema de inyección de dependencias. Físicamente, esos objetos residen en un componente denominado «contenedor de beans».

Spring puede dotar a los métodos de los beans de superpoderes tales como el envío y la recepción de mensajes\eventos, transaccionalidad o ejecución asíncrona. Beneficios adicionales al verdadero poder de la inyección de dependencias: facilitar la construcción de todo un sistema con clases lo más independientes (desacopladas) e intercambiables posibles. Incluso podemos —y debemos— incrementar la abstracción definiendo las operaciones de nuestros beans con interfaces:

public interface CountryService {

    List<Country> findAll();

@Service
public class CountryServiceImpl implements CountryService {

Tras este cambio, pasaríamos a inyectar la interfaz CountryService en aquellos beans que precisen de sus métodos. Dependeremos de un contrato en vez de una clase concreta, lo que permite elegir cualquier implementación disponible.

Ejecutando el proyecto. La anotación @SpringBootApplication.

Antes de dar el salto al mundo web, veamos cómo ejecutar el proyecto.

Spring Boot precisa de un método main estándar situado en una clase anotada con @SpringBootApplication. Se trata de una «meta-anotación» que incluye las funcionalidades de las tres que siguen:

  • @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 iniciadores del proyecto.
  • @ComponentScan. Activa la búsqueda y empleo automático de los beans de Spring que hayamos definido en el proyecto.

Dentro del método main arrancamos Spring invocando al método estático SpringApplication#run. Sus argumentos consisten en una clase de configuración inicial (lo común es usar la clase @SpringBootApplication) y los parámetros pasados a la jvm de 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 almacenados en el sistema usando un bean de la interfaz CountryService. Fíjate que he escrito almacenados «en el sistema» y no «en la base de datos» o cualquier otra ubicación concreta. Este conocimiento es un detalle privado de las implementaciones de CountryService; los usuarios de la interfaz se limitan a llamar a sus métodos. Las virtudes de la inyección de dependencia en acción.

Inyectamos CountryService con @Autowired. Recibiremos CountryServiceImpl, su única implementación:

@SpringBootApplication
public class CountriesApp {

    @Autowired
    CountryService 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()));
    }
 
}

Así queda, de momento, la estructura del proyecto.

Ya estamos listos para ejecutar el método main desde nuestro IDE favorito y comprobar la bitácora. Antes, conviene revisar que los parámetros de conexión a MySQL están bien configurados, en especial el puerto, y que existan 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, recuerda, dimos a la propiedad spring.jpa.hibernate.ddl-auto el valor update.

Si todo marcha bien, 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

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

spring-boot:repackage

El pequeño es el artefacto estándar que genera Maven (opción package) y contiene solo las clases propias del proyecto. El grande lo crea el plugin de Spring Boot (opción spring-boot:repackage) a partir del pequeño y puede ejecutarse con java -jar.

En el proyecto de ejemplo, al contar con spring-boot-maven-plugin en el pom, basta con ejecutar mvn package para se construya el .jar ejecutable.

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

mvn spring-boot:run

Servicios REST

Implementemos los servicios REST que abarcan el juego de operaciones CRUD para los países: consulta, creación, actualización y borrado. Todo lo que veremos son funcionalidades del módulo 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

El módulo 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 que sigue una petición HTTP en Spring MVC.

DispatcherServlet es el «cerebro» de Spring MVC. Recibe las peticiones HTTP y las envía al controlador responsable de procesar la url solicitada. Los controladores son beans marcados con @Controller que 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:

@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;
    }

CountryRestController es un bean de Spring en el que inyectamos el servicio de países. La anotación @RequestMapping, 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 y que esté marcado con dos anotaciones:

  • Una que indique el método o verbo HTTP (GET, POST, PUT…) al que atiende. En el ejemplo, se trata de @GetMapping. Puede incluir la url relativa que procesa el método y que se añade a la declarada 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 el endpoint devolverá un documento JSON (de ello se encargará Spring) en vez de una vista generada por ViewResolver.

Lo habitual y lógico es que todas las urls gestionadas por los métodos de un controlador o devuelvan páginas web o bien sean endpoints de una API REST. Por ello, en este último caso lo más cómodo sería marcar la clase con @ResponseBody para que la anotación se aplique a todos los métodos. Y mejor aún recurrir a @RestController, una anotación de conveniencia que incluye @Controller y @ResponseBody. Usémosla para aumentar la claridad del código: viendo @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. En primer lugar, porque lo habitual es que no se correspondan con la estructura de datos que queremos enviar. Segundo, 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 de la API REST. En cualquier caso, para nuestro ejemplo tan sencillo 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 continúa siendo autoejecutable porque se empaqueta junto a un servidor Apache Tomcat.

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

Las aplicaciones web Java se basan en servlets, unas 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. Por ejemplo, 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, siendo Tomcat el más popular. 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.

Un ejemplo de la respuesta:

[
    {      
        "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-. Conseguir 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>

Tras estos cambios, Maven produce de nuevo dos artefactos; pero en esta ocasión son de tipo .war. Tomaremos el pequeño porque no contiene las librerías de Tomcat y de autoejecución. De esto se deduce que solo el grande es autoejecutable.

Para ejecutar la aplicación en un servidor Tomcat, se debe colocar su archivo .war en la carpeta /{tomcat}/webapps y arrancar Tomcat. Este proceso de ejecución efectuado por un servidor se conoce de manera genérica como «despliegue» o deploy.

Obtención de un país por su identificador

Incorporemos un segundo servicio que devuelva 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. Recordemos que la url completa se obtiene añadiendo la de @GetMapping a la indicada en @RequestMapping.

Al ser el identificador un parámetro variable, le damos un nombre y lo envolvemos 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, nunca en la url, con @RequestParam. De nuevo, si el nombre del argumento coincide con el esperado no hay que indicarlo.

Los argumentos marcados con @PathVariable y @RequestParam son obligatorios. Si no se reciben, se lanzarán, de forma respectiva, las excepciones MissingPathVariableException y MissingServletRequestParameterException. Cuando queramos que sean opcionales estableceremos la propiedad required de las anotaciones a false:

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

Como alternativa, podemos declarar como Optional los argumentos del método que recogen esas variables:

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

En el caso de @RequestParam, se puede definir un valor predeterminado con defaultValue:

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

Volvamos al método getById. Nótese que si el país solicitado no existe, devuelve null. Esto no supone un problema para Spring MVC. Dicho esto, si queremos diseñar una buena API REST, lo apropiado 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 detalle la respuesta de los servicios (header, código de estado, body). Está tipada para la clase devuelta en el body.

Mejor ahora:

@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));
}

Conseguimos un código aún más sucinto construyendo la respuesta con los métodos estáticos de ResponseEntity basados 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 país. Es un servicio de tipo POST que espera recibir un JSON como el que sigue:

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

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

package com.danielme.springboot.model;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

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

  //getters y setters

La declaración de la clase resulta intuitiva. Establece que el nombre del país no puede ser vacío, que no superará los 255 caracteres (tamaño máximo de la columna en la base de datos, hay que vigilar estos detalles) y que la población es un valor obligatorio comprendido en cierto rango.

Se requiere este iniciador:

<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 @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 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»).

Se me ocurren dos posibles mejoras para el servicio REST:

  • Deberíamos modelar la respuesta con una clase. No obstante, 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 (la columna name de la tabla countries no admite nombres duplicados), Hibernate lanzará una excepción y el servicio responderá con un estado 500 (error interno). Es más elegante y educado comprobar la existencia del país, de tal modo que cuando exista se informe 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 correcta que no devuelve contenido en el body de la respuesta. 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, frecuentes 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 mecanismo 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 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 sincroniza automáticamente las entidades presentes en ella con la base de datos. Detecta que country se modificó, así que lanzará una sentencia SQL de UPDATE en el instante en que lo considere adecuado. Este funcionamiento lo explico aquí (gestor de entidades) y aquí (la anotación @Transactional).

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, no se devuelve un 404, pues la operación se considera 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. Nos ahorra crear un ResponseEntity. En ejemplos anteriores, esta solución no sirve al ser el código de la respuesta 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:

  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 con clases anotadas con @Entity las tablas de la base de datos y las relaciones entre ellas.
  4. Creamos los repositorios de Spring Data JPA, vinculados a las clases de tipo entidad, 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 (o deberían) el trabajo en clases de servicio y equivalentes. 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 un objeto tal cual o bien contenido en un ResponseEntity. Jackson convertirá esa respuesta a un documento JSON.
  7. Las aplicaciones Spring Boot se ejecutan con un método main situado en una clase marcada 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 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.