SpringBoot
Tutorial SpringBoot: Controlador REST reactivo basado en anotaciones
Aprende a programar un API REST reactiva en Spring WebFlux sobre Spring Boot desarrollando controladores REST reactivos basados en anotaciones utilizando tipos Flux y Mono.
Aprende SpringBoot GRATIS y certifícateMétodos GET usando repositorio reactivo
En Spring WebFlux, los controladores reactivos permiten manejar solicitudes de manera asíncrona y no bloqueante. Para implementar métodos GET que interactúen con un repositorio reactivo, utilizamos las anotaciones estándar de Spring y las clases Flux y Mono para manejar flujos de datos reactivos.
Supongamos que tenemos una entidad Producto
y un repositorio reactivo ProductoRepository
que extiende de ReactiveCrudRepository
:
public interface ProductoRepository extends ReactiveCrudRepository<Producto, String> {
Flux<Producto> findByCategoria(String categoria);
}
En un controlador reactivo, podemos definir métodos para obtener uno o varios productos. Por ejemplo, para obtener todos los productos:
@RestController
@RequestMapping("/productos")
public class ProductoController {
private final ProductoRepository productoRepository;
public ProductoController(ProductoRepository productoRepository) {
this.productoRepository = productoRepository;
}
@GetMapping
public Flux<Producto> obtenerTodos() {
return productoRepository.findAll();
}
}
En este ejemplo, el método obtenerTodos
devuelve un Flux de Producto
, representando un flujo reactivo de productos. La anotación @GetMapping
sin especificar una ruta adicional se asocia con la ruta base /productos
.
Para obtener un producto por su identificador, podemos definir:
@GetMapping("/{id}")
public Mono<Producto> obtenerPorId(@PathVariable String id) {
return productoRepository.findById(id);
}
Aquí, obtenerPorId
devuelve un Mono de Producto
, que emitirá el producto si se encuentra o completará vacío si no existe.
Si queremos filtrar productos por una propiedad, como la categoría, utilizamos parámetros de consulta:
@GetMapping("/buscar")
public Flux<Producto> buscarPorCategoria(@RequestParam String categoria) {
return productoRepository.findByCategoria(categoria);
}
En este caso, el método buscarPorCategoria
recibe un parámetro de solicitud categoria
y devuelve un Flux con los productos que coinciden.
Es importante destacar que al devolver Flux y Mono, Spring WebFlux maneja automáticamente la suscripción y la publicación de los datos en el cuerpo de la respuesta HTTP. Además, al utilizar los repositorios reactivos de Spring Data, obtenemos una integración fluida con las bases de datos que soportan operaciones no bloqueantes.
Para manejar escenarios donde el producto no existe, podemos utilizar operadores reactivos como switchIfEmpty
y devolver una respuesta adecuada:
@GetMapping("/{id}")
public Mono<ResponseEntity<Producto>> obtenerPorId(@PathVariable String id) {
return productoRepository.findById(id)
.map(producto -> ResponseEntity.ok(producto))
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
}
En este ejemplo, al envolver el Mono en un ResponseEntity
, podemos controlar más fácilmente el código de estado HTTP y devolver un 404 Not Found
si el producto no se encuentra.
También podemos manejar flujos infinitos o streamings de datos. Por ejemplo, si queremos emitir productos en tiempo real:
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<Producto> streamProductos() {
return productoRepository.findAll();
}
Al especificar produces = MediaType.TEXT_EVENT_STREAM_VALUE
, indicamos que este endpoint emitirá un flujo de eventos del lado del servidor (Server-Sent Events).
Al usar estas técnicas, aprovechamos al máximo las capacidades de programación reactiva en Spring Boot 3, creando aplicaciones eficientes y escalables.
Métodos POST, PUT y PATCH usando repositorio reactivo
En Spring WebFlux, los controladores reactivos permiten manejar solicitudes HTTP de manera asíncrona y no bloqueante. Al trabajar con métodos POST, PUT y PATCH, utilizamos los repositorios reactivos para interactuar con la base de datos de forma eficiente.
Supongamos que tenemos una entidad Producto
y un repositorio reactivo ProductoRepository
que extiende ReactiveCrudRepository
:
public interface ProductoRepository extends ReactiveCrudRepository<Producto, String> {
// Métodos personalizados si es necesario
}
Manejo de solicitudes POST
Para crear un nuevo Producto
, definimos un método en el controlador reactivo que maneje las solicitudes POST:
@RestController
@RequestMapping("/productos")
public class ProductoController {
private final ProductoRepository productoRepository;
public ProductoController(ProductoRepository productoRepository) {
this.productoRepository = productoRepository;
}
@PostMapping
public Mono<ResponseEntity<Producto>> crearProducto(@RequestBody Producto producto) {
return productoRepository.save(producto)
.map(productoGuardado -> ResponseEntity
.created(URI.create("/productos/" + productoGuardado.getId()))
.body(productoGuardado));
}
}
En este ejemplo, el método crearProducto
recibe un objeto Producto
en el cuerpo de la solicitud. Utilizamos el repositorio reactivo para guardar el producto en la base de datos y devolvemos un ResponseEntity
con el código de estado 201 Created y la ubicación del nuevo recurso.
Es importante validar el objeto recibido. Podemos utilizar anotaciones de Bean Validation en la clase Producto
y agregar @Valid
en el método:
@PostMapping
public Mono<ResponseEntity<Producto>> crearProducto(@Valid @RequestBody Producto producto) {
// ...
}
Manejo de solicitudes PUT
El método PUT se utiliza para reemplazar completamente un recurso existente. En un entorno reactivo, debemos asegurarnos de que el recurso existe antes de actualizarlo:
@PutMapping("/{id}")
public Mono<ResponseEntity<Producto>> actualizarProducto(@PathVariable String id, @Valid @RequestBody Producto producto) {
return productoRepository.findById(id)
.flatMap(productoExistente -> {
producto.setId(id);
return productoRepository.save(producto);
})
.map(productoActualizado -> ResponseEntity.ok(productoActualizado))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
En este método, buscamos el Producto
por su identificador. Si existe, lo actualizamos y guardamos los cambios. Si no se encuentra, devolvemos una respuesta 404 Not Found.
Manejo de solicitudes PATCH
El método PATCH se utiliza para actualizaciones parciales de un recurso. Dado que en Java no existe un mecanismo estándar para aplicar parches, podemos recibir un Map
con las propiedades a actualizar:
@PatchMapping("/{id}")
public Mono<ResponseEntity<Producto>> actualizarProductoParcial(@PathVariable String id, @RequestBody Map<String, Object> campos) {
return productoRepository.findById(id)
.flatMap(productoExistente -> {
campos.forEach((key, value) -> {
Field field = ReflectionUtils.findField(Producto.class, key);
if (field != null) {
field.setAccessible(true);
Object valorConvertido = conversionService.convert(value, field.getType());
ReflectionUtils.setField(field, productoExistente, valorConvertido);
}
});
return productoRepository.save(productoExistente);
})
.map(productoActualizado -> ResponseEntity.ok(productoActualizado))
.defaultIfEmpty(ResponseEntity.notFound().build());
}
En este ejemplo, utilizamos ReflectionUtils
para actualizar dinámicamente los campos del Producto
. Es esencial manejar con cuidado este enfoque, ya que puede introducir problemas de seguridad. Se recomienda validar y sanitizar los datos recibidos.
Manejo de errores y validaciones
En aplicaciones reactivas, el manejo de errores es fundamental. Podemos utilizar un ControllerAdvice
reactivo para capturar excepciones y devolver respuestas adecuadas:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Mono<ResponseEntity<Map<String, String>>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errores = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));
return Mono.just(ResponseEntity.badRequest().body(errores));
}
// Otros manejadores de excepciones
}
Con este enfoque, si la validación falla, devolvemos una respuesta con los errores detallados.
Uso de Mono y operaciones reactivas
Es fundamental comprender que al trabajar con repositorios reactivos, las operaciones como save
, findById
y delete
retornan Monos o Fluxes. Al encadenar estas operaciones, podemos aprovechar los operadores reactivos para manejar flujos de datos de manera elegante.
Por ejemplo, si queremos asegurarnos de que no exista un producto con el mismo nombre antes de guardarlo:
@PostMapping
public Mono<ResponseEntity<Producto>> crearProducto(@Valid @RequestBody Producto producto) {
return productoRepository.findByNombre(producto.getNombre())
.flatMap(productoExistente -> Mono.error(new ResponseStatusException(HttpStatus.CONFLICT, "El producto ya existe")))
.switchIfEmpty(productoRepository.save(producto)
.map(productoGuardado -> ResponseEntity
.created(URI.create("/productos/" + productoGuardado.getId()))
.body(productoGuardado))
);
}
Aquí, utilizamos flatMap
, switchIfEmpty
y manejamos errores con ResponseStatusException
para controlar el flujo de la operación.
Actualizaciones condicionales
En ocasiones, es necesario realizar actualizaciones condicionales basadas en ciertos criterios. Podemos combinar operadores reactivos para lograrlo:
@PutMapping("/{id}")
public Mono<ResponseEntity<Producto>> actualizarProductoSiActivo(@PathVariable String id, @Valid @RequestBody Producto producto) {
return productoRepository.findById(id)
.filter(Producto::isActivo)
.flatMap(productoExistente -> {
producto.setId(id);
return productoRepository.save(producto);
})
.map(productoActualizado -> ResponseEntity.ok(productoActualizado))
.defaultIfEmpty(ResponseEntity.status(HttpStatus.FORBIDDEN).build());
}
En este caso, solo actualizamos el producto si está activo, devolviendo un 403 Forbidden en caso contrario.
Métodos DELETE usando repositorio reactivo
En Spring WebFlux, los controladores reactivos permiten manejar solicitudes de manera asíncrona y no bloqueante. Para implementar métodos DELETE que interactúen con un repositorio reactivo, utilizamos las anotaciones estándar de Spring y las clases Mono para manejar operaciones reactivas de eliminación.
Supongamos que tenemos una entidad Producto
y un repositorio reactivo ProductoRepository
que extiende ReactiveCrudRepository
:
public interface ProductoRepository extends ReactiveCrudRepository<Producto, String> {
// Métodos personalizados si es necesario
}
Eliminación de un producto por ID
Para eliminar un producto por su identificador, definimos un método en el controlador que maneje las solicitudes DELETE:
@RestController
@RequestMapping("/productos")
public class ProductoController {
private final ProductoRepository productoRepository;
public ProductoController(ProductoRepository productoRepository) {
this.productoRepository = productoRepository;
}
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> eliminarProducto(@PathVariable String id) {
return productoRepository.findById(id)
.flatMap(productoExistente ->
productoRepository.delete(productoExistente)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)))
)
.defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));
}
}
En este ejemplo, el método eliminarProducto
busca el producto por su ID utilizando findById
. Si el producto existe, se elimina llamando a delete
y se devuelve una respuesta con el código de estado 204 No Content. Si no se encuentra, se devuelve una respuesta 404 Not Found. El uso de then
permite encadenar acciones de forma reactiva, asegurando que la respuesta solo se envíe después de completar la eliminación.
Eliminación de todos los productos
Si deseamos proporcionar una operación para eliminar todos los productos, podemos definir:
@DeleteMapping
public Mono<ResponseEntity<Void>> eliminarTodosLosProductos() {
return productoRepository.deleteAll()
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)));
}
Aquí, deleteAll
elimina todos los registros de la entidad Producto
. Una vez completada la operación, devolvemos una respuesta con el estado 204 No Content. Es importante usar then
para asegurarnos de que la respuesta se envíe después de finalizar la eliminación.
Manejo de errores y control de flujo reactivo
Al trabajar con operaciones reactivas, el manejo de errores y el control del flujo son esenciales. Por ejemplo, si queremos manejar excepciones durante la eliminación, podemos utilizar el operador onErrorResume
:
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> eliminarProducto(@PathVariable String id) {
return productoRepository.findById(id)
.flatMap(productoExistente ->
productoRepository.delete(productoExistente)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)))
)
.defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND))
.onErrorResume(error -> Mono.just(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)));
}
Con onErrorResume
, capturamos cualquier error que ocurra durante la operación y devolvemos una respuesta de error apropiada.
Eliminación condicional
Podemos implementar una eliminación condicional basada en ciertos criterios. Por ejemplo, eliminar un producto solo si está inactivo:
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> eliminarProductoSiInactivo(@PathVariable String id) {
return productoRepository.findById(id)
.filter(producto -> !producto.isActivo())
.flatMap(productoInactivo ->
productoRepository.delete(productoInactivo)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)))
)
.switchIfEmpty(Mono.just(new ResponseEntity<>(HttpStatus.FORBIDDEN)));
}
En este caso, utilizamos filter
para comprobar si el producto no está activo. Si cumple la condición, procedemos a eliminarlo; de lo contrario, devolvemos una respuesta 403 Forbidden.
Uso de eliminaciones en bloque
Si necesitamos eliminar múltiples productos basados en un criterio, podemos utilizar métodos personalizados en el repositorio reactivo:
public interface ProductoRepository extends ReactiveCrudRepository<Producto, String> {
Flux<Producto> findByCategoria(String categoria);
Mono<Void> deleteByCategoria(String categoria);
}
Implementamos el método deleteByCategoria
y en el controlador:
@DeleteMapping("/categoria/{categoria}")
public Mono<ResponseEntity<Void>> eliminarPorCategoria(@PathVariable String categoria) {
return productoRepository.deleteByCategoria(categoria)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)));
}
Este método elimina todos los productos pertenecientes a una categoría específica. Al usar operaciones reactivas, garantizamos que la eliminación sea eficiente y no bloqueante.
Consideraciones de concurrencia y consistencia
Al eliminar recursos en aplicaciones reactivas, es fundamental considerar la concurrencia y mantener la consistencia de los datos. Por ejemplo, si otro proceso modifica un producto antes de su eliminación, podríamos implementar un control optimista mediante versiones:
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> eliminarProductoConControl(@PathVariable String id, @RequestHeader("If-Match") String version) {
return productoRepository.findById(id)
.filter(producto -> producto.getVersion().equals(version))
.flatMap(productoExistente ->
productoRepository.delete(productoExistente)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)))
)
.switchIfEmpty(Mono.just(new ResponseEntity<>(HttpStatus.PRECONDITION_FAILED)));
}
En este ejemplo, utilizamos el encabezado If-Match
para recibir la versión del producto. Solo se eliminará si la versión coincide, evitando conflictos. Si no coincide, devolvemos un 412 Precondition Failed.
Uso de métodos HTTP idempotentes
El método DELETE es idempotente, lo que significa que múltiples solicitudes idénticas deberían tener el mismo efecto que una sola. Por lo tanto, si intentamos eliminar un recurso que ya no existe, es recomendable devolver un 204 No Content en lugar de un 404 Not Found:
@DeleteMapping("/{id}")
public Mono<ResponseEntity<Void>> eliminarProductoIdempotente(@PathVariable String id) {
return productoRepository.deleteById(id)
.then(Mono.just(new ResponseEntity<Void>(HttpStatus.NO_CONTENT)))
.onErrorResume(DataAccessException.class, e -> Mono.just(new ResponseEntity<>(HttpStatus.NO_CONTENT)));
}
Aquí, usamos deleteById
, que devuelve un Mono<Void>
. Si el ID no existe, la operación sigue siendo exitosa desde la perspectiva del cliente, y devolvemos 204 No Content.
Integración con seguridad y autorización
En entornos productivos, es común restringir las operaciones de eliminación a usuarios con permisos específicos. Podemos usar anotaciones de Spring Security para proteger nuestros endpoints:
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public Mono<ResponseEntity<Void>> eliminarProducto(@PathVariable String id) {
// Implementación del método
}
La anotación @PreAuthorize
verifica que el usuario tenga el rol ADMIN antes de permitir la operación.
Buenas prácticas en operaciones DELETE reactivas
- Validación previa: Antes de eliminar, es recomendable verificar que el recurso existe y que cumple ciertas condiciones.
- Manejo de excepciones: Utilizar operadores reactivos para capturar y manejar errores, proporcionando respuestas significativas al cliente.
- Consistencia de datos: Implementar controles para evitar inconsistencias, especialmente en operaciones concurrentes.
- Uso de Mono y Flux: Aprovechar las capacidades reactivas para encadenar operaciones y manejar flujos de datos eficientemente.
- Documentación de la API: Utilizar herramientas como OpenAPI para documentar los endpoints y proporcionar información clara sobre las respuestas y códigos de estado.
Al seguir estas prácticas, garantizamos que nuestras operaciones DELETE con repositorios reactivos en Spring Boot 3 sean robustas, eficientes y seguras.
Ejercicios de esta lección Controlador REST reactivo basado en anotaciones
Evalúa tus conocimientos de esta lección Controlador REST reactivo basado en anotaciones con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
API Query By Example (QBE)
Identificadores y relaciones JPA
Borrar datos de base de datos
Web y Test Starters
Métodos find en repositorios
Controladores Spring MVC
Inserción de datos
CRUD Customers Spring MVC + Spring Data JPA
Backend API REST con Spring Boot
Controladores Spring REST
Uso de Spring con Thymeleaf
API Specification
Registro de usuarios
Crear entidades JPA
Asociaciones en JPA
Asociaciones de entidades JPA
Integración con Vue
Consultas JPQL
Open API y cómo agregarlo en Spring Boot
Uso de Controladores REST
Repositorios reactivos
Inyección de dependencias
Introducción a Spring Boot
CRUD y JPA Repository
Inyección de dependencias
Vista en Spring MVC con Thymeleaf
Servicios en Spring
Operadores Reactivos
Configuración de Vue
Entidades JPA
Integración con Angular
API Specification
API Query By Example (QBE)
Controladores MVC
Anotaciones y mapeo en JPA
Consultas JPQL con @Query en Spring Data JPA
Repositorios Spring Data
Inyección de dependencias
Data JPA y Mail Starters
Configuración de Angular
Controladores Spring REST
Configuración de Controladores MVC
Consultas JPQL con @Query en Spring Data JPA
Actualizar datos de base de datos
Verificar token JWT en peticiones
Login de usuarios
Integración con React
Configuración de React
Todas las lecciones de SpringBoot
Accede a todas las lecciones de SpringBoot y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Spring Boot
Introducción Y Entorno
Spring Boot Starters
Introducción Y Entorno
Inyección De Dependencias
Introducción Y Entorno
Controladores Spring Mvc
Spring Web
Vista En Spring Mvc Con Thymeleaf
Spring Web
Controladores Spring Rest
Spring Web
Open Api Y Cómo Agregarlo En Spring Boot
Spring Web
Servicios En Spring
Spring Web
Clientes Resttemplate Y Restclient
Spring Web
Rxjava En Spring Web
Spring Web
Crear Entidades Jpa
Persistencia Spring Data
Asociaciones De Entidades Jpa
Persistencia Spring Data
Repositorios Spring Data
Persistencia Spring Data
Métodos Find En Repositorios
Persistencia Spring Data
Inserción De Datos
Persistencia Spring Data
Actualizar Datos De Base De Datos
Persistencia Spring Data
Borrar Datos De Base De Datos
Persistencia Spring Data
Consultas Jpql Con @Query En Spring Data Jpa
Persistencia Spring Data
Api Query By Example (Qbe)
Persistencia Spring Data
Api Specification
Persistencia Spring Data
Repositorios Reactivos
Persistencia Spring Data
Introducción E Instalación De Apache Kafka
Mensajería Asíncrona
Crear Proyecto Con Apache Kafka
Mensajería Asíncrona
Creación De Producers
Mensajería Asíncrona
Creación De Consumers
Mensajería Asíncrona
Kafka Streams En Spring Boot
Mensajería Asíncrona
Introducción A Spring Webflux
Reactividad Webflux
Spring Data R2dbc
Reactividad Webflux
Controlador Rest Reactivo Basado En Anotaciones
Reactividad Webflux
Controlador Rest Reactivo Funcional
Reactividad Webflux
Operadores Reactivos Básicos
Reactividad Webflux
Operadores Reactivos Avanzados
Reactividad Webflux
Cliente Reactivo Webclient
Reactividad Webflux
Introducción A Spring Security
Seguridad Con Spring Security
Seguridad Basada En Formulario En Mvc Con Thymeleaf
Seguridad Con Spring Security
Registro De Usuarios
Seguridad Con Spring Security
Login De Usuarios
Seguridad Con Spring Security
Verificar Token Jwt En Peticiones
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Spring Web
Seguridad Con Spring Security
Seguridad Jwt En Api Rest Reactiva Spring Webflux
Seguridad Con Spring Security
Autenticación Y Autorización Con Anotaciones
Seguridad Con Spring Security
Testing Unitario De Componentes Y Servicios
Testing Con Spring Test
Testing De Repositorios Spring Data Jpa
Testing Con Spring Test
Testing Controladores Spring Mvc Con Thymeleaf
Testing Con Spring Test
Testing Controladores Rest Con Json
Testing Con Spring Test
Testing De Aplicaciones Reactivas Webflux
Testing Con Spring Test
Testing De Seguridad Spring Security
Testing Con Spring Test
Testing Con Apache Kafka
Testing Con Spring Test
Integración Con Angular
Integración Frontend
Integración Con React
Integración Frontend
Integración Con Vue
Integración Frontend
En esta lección
Objetivos de aprendizaje de esta lección
- Creación API REST
- Reactividad en API REST
- Conexión controlador con repositorios
- Operadores reactivos
- Gestión de errores en controladores reactivos