Introducción a Spring Boot: Aplicación con servicios REST y Spring Data JPA

Última actualización: 30/07/2022

logo springMás que un marco de trabajo, Spring es un auténtico ecosistema de desarrollo compuesto de decenas de módulos integrables entre sí que 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, inevitablemente, de Spring Boot.

En este tutorial veremos en qué consiste y daremos los primeros pasos para comenzar a utilizar Spring con Spring Boot 2.7. En concreto, crearemos una aplicación web Maven (Spring Boot también es compatible con Gradle) con una pequeña API REST para explotar una tabla de una base de datos MySQL con información sobre países.

Con respecto al testing 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.

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

¿Por qué 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. Además, la configuración puede complicarse si, por ejemplo, vamos a desarrollar un proyecto web completo y necesitamos también utilizar otros módulos de Spring tales como MVC, Data, Security, etcétera. Muchas dependencias que a su vez dependen de otras, y todas deben funcionar en conjunto de forma 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 recurrir a plantillas de terceros, como las indexadas por yeoman, o irnos directamente a marcos de trabajo basados en Spring, tales como JHipster o Grails.

Teniendo en cuanta esta problemática, allá por 2014 se publicó la primera versión estable de Spring Boot, con el loable objetivo 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».

Hay que tener presente que Spring Boot no es un framework al uso ni un generador de código. Se centra en la configuración e integración de las librerías que solemos necesitar de forma genérica para que empecemos rápidamente a desarrollar nuestra aplicación. Incluso configura un servidor Tomcat o Jetty si así lo deseamos. Y lo mejor de todo es que no supone una limitación, ya que podemos seguir realizando cualquier configuración de Spring de igual forma que si no tuviéramos Spring Boot.

Creando un proyecto con Spring Boot para Spring Data JPA

La creación de proyectos Spring Boot se puede hacer vía web con Spring initializr, o bien con los asistentes de Eclipse (plugin Spring Tool Suite) e IntelliJ (versión Ultimate). No obstante, voy a crear y configurar paso a paso y a mano un proyecto Maven para ir explicando al lector los fundamentos de Spring Boot.

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») de Spring Boot que incluyen todo lo necesario. Las versiones son seleccionadas y probadas con esmero por el equipo de Spring Boot para que todo el sistema funcione sin incompatibilidades.

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

Añadimos lo siguiente al pom.xml:

  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, pero podemos cambiarla a una superior.
    <properties>
        <java.version>11</java.version>
    </properties>
    
  3. Añadir los starters que encapsulen 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>
    
  4. No podemos olvidar el controlador o driver JDBC con la API que permitirá a Hibernate interactuar con MySQL.
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
    </dependency>
    
  5. No es imprescindible, pero sí recomendable, añadir el plugin de Maven de Spring Boot que nos ayudará a ejecutar y empaquetar nuestras aplicaciones. Otra funcionalidad muy 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í. Será modificado a medida que avancemos 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>
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. Lo siguiente es un ejemplo muy básico que escribe toda la información hasta el nivel DEBUG inclusive en la salida estándar de Java.

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT"
        class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{MM-dd-HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n
            </pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

Para configuraciones tan simples contamos con la posibilidad de configurar el sistema de logging 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 se utiliza Lombok, disponemos 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. Al crearse un contenedor, se configura todo lo necesario porque se ejecutará el script init.sql

Con estas órdenes se contruye 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á usar un puerto distinto. Si estás en esta situación, usa uno distinto al crear el contenedor y tenlo en cuenta al configurar los datos de 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 JDBC. Esta API cuenta con proveedores para las distintas bases de datos del mercado (MySQL, PostgreSQL, Oracle…). Trabajar con ella es muy tedioso y consume mucho tiempo porque requiere escribir todas las sentencias en SQL que necesitemos, transformar datos, gestionar excepciones, transacciones, etcétera. Sé de lo que hablo, pues invertí incontables horas de mi vida en estas tareas antes de que se popularizaran herramientas como Hibernate o MyBatis.

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

En nuestro proyecto, esto se traduce en una interfaz que especializa a JpaRepository y tipada para la entidad Country y su identificador. Será más que suficiente con los métodos que nos 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, consultar 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.

Parámetros de configuración

Ahora pasamos a definir los parámetros de configuración de los distintos módulos de Spring y de otras librerías que estemos usando. Spring Boot lo pone fácil al centralizar toda la configuración 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 permite interactuar con una base de datos MySQL mediante JPA e Hibernate.

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, que por omisión es none, a update para que se sincronicen nuestras entidades JPA con la estructura de la base de datos (¡No hacer esto en producción si no tenemos claro lo que estamos haciendo!).

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

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 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 anotadas con @Component (o anotaciones que la incluyan como @Service, @Controller…) o aquellas cuyos objetos sean construidos en métodos 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

Antes de dar el salto al mundo web, veamos cómo ejecutar el proyecto. El punto de entrada de un proyecto Spring Boot es, ni más ni menos, que una clase Main. Eso sí, la anotamos con @SpringBootApplication. Además de arrancar Spring cuando se ejecute el método main, aplica las funcionalidades proporcionadas por @Configuration, @EnableAutoConfiguration y @ComponentScan, entre algunas otras.

Dentro del método main lanzamos la aplicación.

@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! Vamos a escribir en la bitácora los países de la tablas countries para lo cual tenemos que inyectar CountryService.

Para ejecutar código en el arranque después del inicio de Spring, evitando la limitación que implica el hecho de que main sea estático, implementaremos la interfaz CommandLineRunner.

@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á automáticamente 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. Tras la ejecución de run, la aplicación queda detenida.

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.

Antes de empezar a escribir código, tenemos que convertir nuestro proyecto en uno de tipo web añadiendo el starter correspondiente.

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

Cada servicio REST se implementa en un método de una clase «controladora» anotada con @Controller. De forma predeterminada, Spring asume que estos métodos van a devolver el nombre de una vista (JSP, Thymeleaf, etcétera) que generará una página web que será enviada al solicitante de la petición HTTP. En el caso de REST, los métodos devolverán directamente la respuesta, circunstancia que indicamos con la anotación @ResponseBody. Podemos ahorrárnosla si, en vez de @Controller, usamos @RestController.

@RestController
@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. Se usa para establecer una url raíz común a todos los servicios REST definidos en la clase.

Veamos el primer método.

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

Magnífico. Para obtener la lista vía REST, tan solo se precisa de un método que la devuelva. @GetMapping indica que las peticiones atendidas son de tipo GET.

Generalmente no querremos 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 innecesariamente a los clientes externos. En cualquier caso, para nuestro ejemplo nos sirve.

Ejecución

¿Cómo ejecutamos un proyecto Spring Boot de tipo web? Pues como ya sabemos: a través de su Main. La aplicación sigue siendo autoejecutable porque se empaqueta junto a un servidor Tomcat. De nuevo, su magia facilita nuestro trabajo.

Ese Tomcat atiende al puerto 8080 y despliega la aplicación en su raíz. Podemos cambiar esta configuración con las propiedades

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

He dejado la configuración predeterminada en el proyecto de ejemplo, por lo que el servicio se encuentra en http://localhost:8080/api/countries y devolverá algo así.

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

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

List<Country> se ha transformado (serializado) en un JSON. Spring MVC realiza la conversión entre objetos y JSON con Jackson. Se puede personalizar, cómo no, en el application.properties.

No estamos obligados a utilizar la característica de autoejecución de Spring Boot y podemos construir nuestra aplicación de forma «tradicional» creando un fichero de tipo .war desplegable en un servidor web Java compatible. Tan sencillo como:

  • Hacer que la clase Main herede de SpringBootServletInitializer.
    @SpringBootApplication
    public class CountriesApp extends SpringBootServletInitializer
    {
    
  • Cambiar en el pom el tipo de empaquetado a war.
    <packaging>war</packaging>
    
  • Añadir el starter 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 despleguemos 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. Aunque ambos pueden desplegarse en un servidor, usaremos el más pequeño porque no contiene las librerias de Tomcat y de autoejecución. De este consejo se deduce que solo el más grande es autoejecutable.

¡Más servicios!

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 en la anotación @GetMapping. Por ser un parámetro variable «incrustado» en la url, le damos un nombre contenido entre «{}». Lo recibimos en el método declarando un argumento del tipo adecuado anotado con @PathVariable. Si el nombre de ese argumento coincide con el usado en la url, no hace falta indicarlo en la anotación.

Si tuviéramos parámetros establecidos en la parte de consulta de la url (?name=Spain), basta con declararlos en el método con la anotación @RequestParam.

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

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.

@GetMapping("/search")
public List<Country> search(@RequestParam(required=false) String name) {
@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, devolvemos 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, así que nos 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. ResponseEntity contiene una clase de tipo Builder que simplifica la creación de sus objetos.

@GetMapping(value = "/{id}")
public ResponseEntity<Country> getById(@PathVariable("id") Long id) {
    return countryService
            .findById(id)
            .map(ResponseEntity::ok)
            .orElseGet(() -> ResponseEntity.notFound().build());
}

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. En consecuencia, 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>

En el método del controlador recibimos un objeto CountryRequest añadiéndolo como un argumento anotado con @RequestBody. Al igual que @PathVariable y @RequestParam, cuenta con la propiedad required (por omisión es true) para establecer la obligatoriedad de la existencia del parámetro, que en este caso es el body de la petición.

Asimismo, acompañamos la declaración del argumento con @Valid para que se apliquen las validaciones definidas con anotaciones. Si se incumplen, se devuelve automáticamente el código HTTP 400 y un JSON detallando los problemas. Si la inserción es correcta, retornaremos el identificador del nuevo país en un JSON con el atributo id. El código para estos casos es 201 («creado»).

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

Tres 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.
  • Aquí explico cómo personalizar el JSON con los errores de validación.

La actualización es parecida a la creación, con una llamada PUT a la url del país. No se devuelve nada, por lo que el tipo de la respuesta es Void y el código HTTP es 204: operación exitosa sin contenido en el body de la respuesta. Pero cuando el país a actualizar no existe, 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) {
    Optional<Country> country = countryRepository.findById(id);
    if (country.isPresent()) {
        country.get().setName(countryRequest.getName());
        country.get().setPopulation(countryRequest.getPopulation());
        return true;
    }
    return false;
}

Podríamos debatir si 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, que serán habituales cuando la API vaya creciendo con nuevas funcionalidades. Aplico esa solución aquí.

Sin embargo, 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. Sabe que la entidad country se ha modificado y lanzará una sentencia SQL de UPDATE. Este funcionamiento lo explico con detalle aquí.

Cierro 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 la eliminación de uno que no existe, no se devuelve un 404 por ser, precisamente, lo que se quiere: que el país no exista.

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

Con @ResponseStatus se establece el código de la respuesta. En este caso concreto, nos ahorramos tener que devolver ResponseEntity. En ejemplos anteriores no nos sirve esta solución porque el código de respuesta no es siempre el mismo.

Código de ejemplo

El proyecto completo para el presente tutorial y también Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos embebidas se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Otros tutoriales relacionados con Spring

Spring REST: Securización BASIC y JDBC con Spring Security

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 embebidas

Spring JDBC Template: simplificando el uso de SQL

Persistencia en BD con Spring Data JPA

Persistencia en BD con Spring: Integrando JPA, c3p0, Hibernate y EHCache

Testing Spring con JUnit 4

Ficheros .properties en Spring IoC

4 comentarios sobre “Introducción a Spring Boot: Aplicación con servicios 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.