SpringBoot
Tutorial SpringBoot: Controlador REST reactivo funcional
Aprende a programar un API REST reactiva en Spring WebFlux sobre Spring Boot desarrollando controladores REST reactivos funcionales con rutas y métodos manejadores.
Aprende SpringBoot GRATIS y certifícateExplicación de clases para controladores funcionales
En Spring WebFlux, los controladores funcionales utilizan un enfoque basado en funciones para manejar las solicitudes HTTP. Este enfoque se basa en varias clases clave que facilitan la creación y gestión de rutas y manejadores de solicitudes.
Una de las clases principales es RouterFunction
, que representa una función que enruta una solicitud a un manejador. Se utiliza junto con la clase RouterFunctions
para definir las rutas de la aplicación. Por ejemplo, podemos usar RouterFunctions.route()
para establecer una ruta específica y asociarla con un manejador.
Otra clase esencial es HandlerFunction
, que es una interfaz funcional que maneja una solicitud y produce una respuesta. Los manejadores se implementan típicamente como funciones lambda o métodos que reciben un ServerRequest
y devuelven un Mono<ServerResponse>
.
El objeto ServerRequest
encapsula la información de la solicitud HTTP entrante, proporcionando métodos para acceder a parámetros, encabezados y el cuerpo de la solicitud. Por otro lado, ServerResponse
se utiliza para construir la respuesta HTTP que se enviará al cliente, permitiendo establecer el código de estado, los encabezados y el cuerpo de la respuesta.
Además, RequestPredicate
es una interfaz funcional que sirve para definir condiciones bajo las cuales se enruta una solicitud a un manejador. Estas condiciones pueden basarse en el método HTTP, la ruta, los encabezados u otros atributos de la solicitud.
Para configurar los controladores funcionales en la aplicación, es común definir una clase de configuración que contiene los métodos para construir las rutas y los manejadores. Esta clase se anota con @Configuration
para que Spring la detecte y procese adecuadamente.
Por ejemplo, podemos tener una configuración como la siguiente:
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> route(Handler handler) {
return RouterFunctions
.route(RequestPredicates.GET("/api/funcional/ejemplo"), handler::manejarEjemplo);
}
}
En este ejemplo, estamos definiendo una ruta que responde a solicitudes GET en el path /api/funcional/ejemplo
, y la estamos asociando con el método manejarEjemplo
del objeto handler
.
El handler
es una clase que implementa los métodos necesarios para manejar las solicitudes. Un ejemplo de un manejador podría ser:
@Component
public class Handler {
public Mono<ServerResponse> manejarEjemplo(ServerRequest request) {
// Lógica del manejador
return ServerResponse.ok().body(BodyInserters.fromValue("Respuesta de ejemplo"));
}
}
Aquí, el método manejarEjemplo
recibe un ServerRequest
y devuelve un Mono<ServerResponse>
, siguiendo el modelo reactivo. Utiliza ServerResponse.ok()
para construir una respuesta exitosa y BodyInserters.fromValue()
para insertar el cuerpo de la respuesta.
Es importante destacar que, al utilizar este enfoque funcional, se aprovechan las capacidades de la programación funcional y reactiva de Java, permitiendo un código más conciso y expresivo.
Por último, para manejar excepciones y errores, se puede utilizar la clase ErrorWebExceptionHandler
, que permite definir cómo se gestionan las excepciones no controladas en la aplicación.
Métodos GET usando handlers funcionales en servicio
Al implementar métodos GET utilizando handlers funcionales en un servicio con Spring WebFlux, se adopta un enfoque más declarativo y funcional para manejar las solicitudes HTTP. Este estilo permite definir rutas y manejadores de forma concisa y expresiva.
Para comenzar, es esencial definir las rutas que atenderán las solicitudes GET. Utilizamos la función route()
de RouterFunctions
para asociar una ruta específica con un handler. Por ejemplo:
@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
return RouterFunctions.route()
.GET("/api/funcional/usuarios", handler::obtenerUsuarios)
.GET("/api/funcional/usuarios/{id}", handler::obtenerUsuarioPorId)
.build();
}
En este ejemplo, estamos configurando dos rutas GET:
/api/funcional/usuarios
: para obtener la lista de usuarios./api/funcional/usuarios/{id}
: para obtener un usuario específico por su identificador.
El siguiente paso es implementar los handlers correspondientes en el servicio. Estos handlers son métodos que reciben un ServerRequest
y devuelven un Mono<ServerResponse>
. Veamos cómo se podrían definir:
@Component
public class Handler {
private final UsuarioService usuarioService;
public Handler(UsuarioService usuarioService) {
this.usuarioService = usuarioService;
}
public Mono<ServerResponse> obtenerUsuarios(ServerRequest request) {
Flux<Usuario> usuarios = usuarioService.listarUsuarios();
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(usuarios, Usuario.class);
}
public Mono<ServerResponse> obtenerUsuarioPorId(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Usuario> usuario = usuarioService.buscarUsuarioPorId(id);
return usuario.flatMap(u -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(u))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
En el handler obtenerUsuarios
, se obtiene un Flux<Usuario>
llamando al servicio listarUsuarios()
. Luego, se construye una respuesta con ServerResponse.ok()
, estableciendo el tipo de contenido como JSON y pasando el flujo de usuarios en el cuerpo de la respuesta.
Para el handler obtenerUsuarioPorId
, extraemos el parámetro {id}
de la ruta utilizando request.pathVariable("id")
. Se busca el usuario con buscarUsuarioPorId(id)
, que devuelve un Mono<Usuario>
. Utilizamos flatMap
para manejar el caso en que el usuario exista y switchIfEmpty
para responder con un 404 Not Found si no se encuentra el usuario.
Es importante destacar el uso de métodos como body()
y bodyValue()
para construir el cuerpo de la respuesta. Mientras que body()
se utiliza con un Publisher
(como Flux
o Mono
), bodyValue()
se usa cuando ya tenemos el objeto a devolver.
Además, para manejar parámetros de consulta en solicitudes GET, podemos utilizar request.queryParam("nombre")
:
public Mono<ServerResponse> buscarUsuariosPorNombre(ServerRequest request) {
String nombre = request.queryParam("nombre").orElse("");
Flux<Usuario> usuarios = usuarioService.buscarUsuariosPorNombre(nombre);
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(usuarios, Usuario.class);
}
En este caso, si el parámetro nombre
no está presente, se utilizará una cadena vacía. El servicio buscarUsuariosPorNombre(nombre)
devuelve un Flux<Usuario>
con los usuarios cuyo nombre coincide con el criterio.
Al utilizar handlers funcionales, también es posible manejar encabezados y otros atributos de la solicitud. Por ejemplo, si necesitamos acceder a un encabezado personalizado:
public Mono<ServerResponse> obtenerUsuarioDesdeEncabezado(ServerRequest request) {
String token = request.headers().firstHeader("Authorization");
Mono<Usuario> usuario = usuarioService.buscarUsuarioPorToken(token);
return usuario.flatMap(u -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(u))
.switchIfEmpty(ServerResponse.status(HttpStatus.UNAUTHORIZED).build());
}
Aquí, extraemos el encabezado Authorization
y buscamos el usuario asociado al token. Si el usuario existe, se devuelve con un 200 OK; de lo contrario, se responde con un 401 Unauthorized.
Cuando se trabaja con datos reactivos, es esencial gestionar adecuadamente el flujo y posibles errores. El uso de operadores como flatMap
, switchIfEmpty
y onErrorResume
permite manejar diferentes escenarios de forma eficiente y reactiva.
Además, podemos aprovechar las expresiones lambda de Java para hacer el código más conciso:
public Mono<ServerResponse> listarUsuarios(ServerRequest request) {
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(usuarioService.listarUsuarios(), Usuario.class);
}
En este ejemplo simplificado, directamente devolvemos la respuesta sin necesidad de asignar el flujo de usuarios a una variable intermedia.
Finalmente, es crucial asegurarse de que los handlers sean sin estado y thread-safe, dado que en aplicaciones reactivas pueden ser llamados concurrentemente. Mantener la lógica de negocio en servicios separados ayuda a mantener una arquitectura limpia y modular.
Métodos POST, PUT y PATCH usando handlers funcionales en servicio
Al implementar métodos POST, PUT y PATCH en Spring WebFlux utilizando handlers funcionales, se adopta un enfoque declarativo y moderno que aprovecha las ventajas de la programación funcional en Java. Estos métodos se utilizan para crear y actualizar recursos respectivamente, y su correcta implementación es esencial para construir APIs RESTful eficientes.
Para comenzar, es necesario definir las rutas que gestionarán las solicitudes HTTP correspondientes. Utilizamos la función route()
de RouterFunctions
para asociar cada ruta con su handler adecuado. Por ejemplo:
@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
return RouterFunctions.route()
.POST("/api/funcional/usuarios", handler::crearUsuario)
.PUT("/api/funcional/usuarios/{id}", handler::actualizarUsuario)
.PATCH("/api/funcional/usuarios/{id}", handler::modificarUsuarioParcialmente)
.build();
}
En este fragmento de código, establecemos tres rutas:
- POST
/api/funcional/usuarios
: para la creación de un nuevo usuario. - PUT
/api/funcional/usuarios/{id}
: para la actualización completa de un usuario existente. - PATCH
/api/funcional/usuarios/{id}
: para la modificación parcial de un usuario.
Una vez definidas las rutas, es fundamental implementar los handlers correspondientes. Estos handlers gestionan el cuerpo de la solicitud, realizan las operaciones necesarias y construyen la respuesta adecuada.
Handler para método POST
El handler para el método POST permite crear un nuevo usuario a partir de los datos proporcionados en el cuerpo de la solicitud:
public Mono<ServerResponse> crearUsuario(ServerRequest request) {
Mono<Usuario> usuarioMono = request.bodyToMono(Usuario.class);
return usuarioMono.flatMap(usuario -> usuarioService.crearUsuario(usuario)
.flatMap(usuarioCreado -> ServerResponse.status(HttpStatus.CREATED)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(usuarioCreado)))
.onErrorResume(error -> ServerResponse.badRequest()
.bodyValue("Error al crear el usuario: " + error.getMessage()));
}
En este handler:
- Utilizamos
bodyToMono(Usuario.class)
para convertir el cuerpo de la solicitud en un objetoUsuario
. - Aplicamos
flatMap
para manipular elMono<Usuario>
resultante y llamar al serviciocrearUsuario(usuario)
. - Si la creación es exitosa, construimos una respuesta con código de estado 201 Created, establecemos el tipo de contenido y enviamos el usuario creado en el cuerpo de la respuesta.
- En caso de error, gestionamos la excepción con
onErrorResume
, devolviendo una respuesta 400 Bad Request con un mensaje descriptivo.
Handler para método PUT
El handler para el método PUT gestiona la actualización completa de un usuario existente:
public Mono<ServerResponse> actualizarUsuario(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Usuario> usuarioMono = request.bodyToMono(Usuario.class);
return usuarioService.buscarUsuarioPorId(id)
.zipWith(usuarioMono, (usuarioExistente, usuarioActualizado) -> {
usuarioActualizado.setId(usuarioExistente.getId());
return usuarioActualizado;
})
.flatMap(usuario -> usuarioService.actualizarUsuario(usuario))
.flatMap(usuarioActualizado -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(usuarioActualizado))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> ServerResponse.badRequest()
.bodyValue("Error al actualizar el usuario: " + error.getMessage()));
}
En este handler:
- Extraemos el
id
del usuario desde el path variable. - Convertimos el cuerpo de la solicitud en un
Mono<Usuario>
. - Buscamos el usuario existente con
buscarUsuarioPorId(id)
. - Utilizamos
zipWith
para combinar el usuario existente con el actualizado, asegurando que elid
se mantenga coherente. - Actualizamos el usuario llamando a
actualizarUsuario(usuario)
del servicio. - Si la operación es exitosa, devolvemos una respuesta 200 OK con el usuario actualizado.
- Si el usuario no existe, respondemos con un 404 Not Found.
- Gestionamos posibles errores devolviendo una respuesta 400 Bad Request.
Handler para método PATCH
El handler para el método PATCH permite modificar parcialmente los datos de un usuario:
public Mono<ServerResponse> modificarUsuarioParcialmente(ServerRequest request) {
String id = request.pathVariable("id");
Mono<Map<String, Object>> updatesMono = request.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {});
return usuarioService.buscarUsuarioPorId(id)
.zipWith(updatesMono, (usuarioExistente, updates) -> {
updates.forEach((key, value) -> {
switch (key) {
case "nombre":
usuarioExistente.setNombre((String) value);
break;
case "email":
usuarioExistente.setEmail((String) value);
break;
// Agregar más campos según sea necesario
}
});
return usuarioExistente;
})
.flatMap(usuario -> usuarioService.actualizarUsuario(usuario))
.flatMap(usuarioActualizado -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(usuarioActualizado))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> ServerResponse.badRequest()
.bodyValue("Error al modificar el usuario: " + error.getMessage()));
}
En este handler:
- Obtenemos el
id
del usuario desde el path variable. - Convertimos el cuerpo de la solicitud en un
Mono<Map<String, Object>>
, lo que nos permite manejar actualizaciones parciales. - Buscamos el usuario existente y utilizamos
zipWith
para combinarlo con el mapa de actualizaciones. - Iteramos sobre las entradas del mapa y actualizamos los campos correspondientes del usuario.
- Llamamos al servicio
actualizarUsuario(usuario)
para persistir los cambios. - Devolvemos una respuesta 200 OK con el usuario modificado o un 404 Not Found si el usuario no existe.
- Manejamos errores y excepciones devolviendo una respuesta 400 Bad Request.
Consideraciones adicionales
Es importante destacar algunos aspectos clave al trabajar con handlers funcionales para métodos POST, PUT y PATCH:
Validación de datos: Antes de procesar la solicitud, es recomendable validar los datos recibidos. Esto puede realizarse mediante librerías de validación o manualmente, verificando que los campos obligatorios estén presentes y cumplan con las restricciones necesarias.
Gestión de errores: Utilizar operadores como onErrorResume
permite manejar excepciones y errores, proporcionando respuestas significativas al cliente y evitando que la aplicación falle silenciosamente.
Inmutabilidad: Al modificar objetos, especialmente en un contexto reactivo, es preferible trabajar con objetos inmutables o crear nuevas instancias para evitar efectos secundarios indeseados.
Uso de Mono y Flux: Los handlers trabajan con Mono cuando se espera un único resultado y con Flux para múltiples resultados. En el caso de POST, PUT y PATCH, generalmente se utiliza Mono<Usuario>
.
Servicios asociados
Para que los handlers funcionen correctamente, es necesario contar con un servicio que gestione la lógica de negocio. Un ejemplo simplificado del servicio podría ser:
@Service
public class UsuarioService {
private final UsuarioRepository usuarioRepository;
public UsuarioService(UsuarioRepository usuarioRepository) {
this.usuarioRepository = usuarioRepository;
}
public Mono<Usuario> crearUsuario(Usuario usuario) {
return usuarioRepository.save(usuario);
}
public Mono<Usuario> buscarUsuarioPorId(String id) {
return usuarioRepository.findById(id);
}
public Mono<Usuario> actualizarUsuario(Usuario usuario) {
return usuarioRepository.save(usuario);
}
}
En este servicio:
crearUsuario(usuario)
guarda un nuevo usuario en el repositorio.buscarUsuarioPorId(id)
busca un usuario existente por suid
.actualizarUsuario(usuario)
persiste los cambios de un usuario existente.
La interacción con el repositorio reactivo permite mantener el flujo reactivo de datos desde la capa de datos hasta la respuesta al cliente.
Respuestas adecuadas
Al construir las respuestas, es esencial establecer el código de estado HTTP correcto:
- 201 Created para indicar que un recurso ha sido creado exitosamente.
- 200 OK para confirmar que una operación ha sido exitosa.
- 400 Bad Request cuando hay errores en la solicitud del cliente.
- 404 Not Found si el recurso solicitado no existe.
Además, es importante establecer el Content-Type apropiado, generalmente MediaType.APPLICATION_JSON
, para que el cliente interprete correctamente la respuesta.
Ejemplo completo de configuración
Integrando todo lo anterior, la configuración completa podría quedar de la siguiente manera:
@Configuration
public class RouterConfig {
@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
return RouterFunctions.route()
.POST("/api/funcional/usuarios", handler::crearUsuario)
.PUT("/api/funcional/usuarios/{id}", handler::actualizarUsuario)
.PATCH("/api/funcional/usuarios/{id}", handler::modificarUsuarioParcialmente)
.build();
}
}
Y los handlers implementados:
@Component
public class Handler {
private final UsuarioService usuarioService;
public Handler(UsuarioService usuarioService) {
this.usuarioService = usuarioService;
}
// Handlers implementados anteriormente...
}
La implementación de métodos POST, PUT y PATCH utilizando handlers funcionales en Spring WebFlux ofrece una forma clara y concisa de gestionar las operaciones de creación y actualización de recursos. Al aprovechar las características de la programación funcional y reactiva, se logra un código más fluido y eficiente, facilitando el desarrollo de servicios web modernos y escalables.
Métodos DELETE usando handlers funcionales en servicio
Al implementar métodos DELETE utilizando handlers funcionales en Spring WebFlux, se sigue un enfoque funcional que permite manejar solicitudes HTTP de forma concisa y expresiva. Este método se utiliza para eliminar recursos identificados por un parámetro, generalmente un identificador único.
Primero, es necesario definir las rutas que atenderán las solicitudes DELETE. Utilizamos el método DELETE
de RouterFunctions.route()
para asociar una ruta específica con su handler correspondiente:
@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
return RouterFunctions.route()
.DELETE("/api/funcional/usuarios/{id}", handler::eliminarUsuario)
.build();
}
En este ejemplo, se configura una ruta DELETE en el path /api/funcional/usuarios/{id}
, donde {id}
es un path variable que representa el identificador del usuario a eliminar. La solicitud será manejada por el método eliminarUsuario
del handler.
A continuación, se implementa el handler que gestionará la lógica de eliminación:
@Component
public class Handler {
private final UsuarioService usuarioService;
public Handler(UsuarioService usuarioService) {
this.usuarioService = usuarioService;
}
public Mono<ServerResponse> eliminarUsuario(ServerRequest request) {
String id = request.pathVariable("id");
return usuarioService.buscarUsuarioPorId(id)
.flatMap(usuario -> usuarioService.eliminarUsuario(usuario.getId())
.then(ServerResponse.noContent().build()))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue("Error al eliminar el usuario: " + error.getMessage()));
}
}
En este handler:
- Se extrae el
id
del usuario desde el path variable de la solicitud. - Se busca el usuario existente utilizando
usuarioService.buscarUsuarioPorId(id)
, que devuelve unMono<Usuario>
. - Si el usuario existe, se procede a eliminarlo llamando a
usuarioService.eliminarUsuario(usuario.getId())
. - La operación de eliminación devuelve un
Mono<Void>
. Al utilizarthen(ServerResponse.noContent().build())
, se espera a que la eliminación se complete y luego se devuelve una respuesta con código de estado 204 No Content. - Si el usuario no se encuentra, se responde con un 404 Not Found utilizando
switchIfEmpty(ServerResponse.notFound().build())
. - Se gestionan los posibles errores con
onErrorResume
, devolviendo una respuesta con código de estado 500 Internal Server Error y un mensaje descriptivo.
El servicio UsuarioService
debe implementar el método eliminarUsuario
que realice la operación de eliminación en el repositorio:
@Service
public class UsuarioService {
private final UsuarioRepository usuarioRepository;
public UsuarioService(UsuarioRepository usuarioRepository) {
this.usuarioRepository = usuarioRepository;
}
public Mono<Usuario> buscarUsuarioPorId(String id) {
return usuarioRepository.findById(id);
}
public Mono<Void> eliminarUsuario(String id) {
return usuarioRepository.deleteById(id);
}
}
En este servicio:
buscarUsuarioPorId(id)
busca el usuario por su identificador y devuelve unMono<Usuario>
.eliminarUsuario(id)
elimina el usuario por suid
y devuelve unMono<Void>
que indica la finalización de la operación.
Es importante manejar adecuadamente la reactividad en estas operaciones. Al utilizar métodos como flatMap
, se asegura que las operaciones sean no bloqueantes y se encadenen de forma correcta en el flujo reactivo.
Gestión de errores y validaciones
Al eliminar recursos, es fundamental verificar que el recurso existe antes de intentar eliminarlo. Además, es aconsejable implementar validaciones adicionales según las necesidades de la aplicación.
Por ejemplo, si se desea verificar que el usuario a eliminar no tiene dependencias que impidan su eliminación, se podría extender el handler de la siguiente manera:
public Mono<ServerResponse> eliminarUsuario(ServerRequest request) {
String id = request.pathVariable("id");
return usuarioService.buscarUsuarioPorId(id)
.flatMap(usuario -> comprobarDependenciasUsuario(usuario)
.flatMap(sinDependencias -> {
if (sinDependencias) {
return usuarioService.eliminarUsuario(usuario.getId())
.then(ServerResponse.noContent().build());
} else {
return ServerResponse.status(HttpStatus.CONFLICT)
.bodyValue("El usuario tiene dependencias y no puede ser eliminado");
}
}))
.switchIfEmpty(ServerResponse.notFound().build())
.onErrorResume(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue("Error al eliminar el usuario: " + error.getMessage()));
}
private Mono<Boolean> comprobarDependenciasUsuario(Usuario usuario) {
// Lógica para comprobar dependencias
return Mono.just(true); // Supongamos que no tiene dependencias
}
En este caso:
- Se añade una comprobación de dependencias mediante el método
comprobarDependenciasUsuario(usuario)
, que devuelve unMono<Boolean>
. - Si el usuario no tiene dependencias, se procede a eliminarlo y se devuelve un 204 No Content.
- Si existen dependencias, se responde con un 409 Conflict y un mensaje descriptivo.
- Se mantiene la gestión de errores y el manejo del caso en que el usuario no existe.
Uso de filtros y middlewares
Es posible incorporar filtros o middlewares en las rutas para añadir funcionalidades transversales, como autenticación o logging. Por ejemplo, para proteger la ruta de eliminación y requerir autenticación:
@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
return RouterFunctions.route()
.DELETE("/api/funcional/usuarios/{id}", handler::eliminarUsuario)
.filter(autenticacionFilter())
.build();
}
private HandlerFilterFunction<ServerResponse, ServerResponse> autenticacionFilter() {
return (request, next) -> {
// Lógica de autenticación
String token = request.headers().firstHeader("Authorization");
if (esTokenValido(token)) {
return next.handle(request);
} else {
return ServerResponse.status(HttpStatus.UNAUTHORIZED).build();
}
};
}
private boolean esTokenValido(String token) {
// Verificación del token
return token != null && token.equals("tokenValido");
}
Aquí:
- Se utiliza
filter(autenticacionFilter())
para aplicar un filtro de autenticación a las rutas. - El filtro comprueba la validez del token de autorización y, si es válido, permite continuar con la solicitud.
- Si el token no es válido, se devuelve una respuesta 401 Unauthorized.
Buenas prácticas
Al implementar métodos DELETE utilizando handlers funcionales, es recomendable seguir ciertas buenas prácticas:
Idempotencia: Aunque las solicitudes DELETE deben ser idempotentes según la especificación HTTP, es importante asegurarse de que múltiples solicitudes para eliminar el mismo recurso no generen errores inesperados.
Seguridad: Proteger las rutas de eliminación con autenticación y autorizaciones adecuadas para evitar eliminaciones no autorizadas.
Respuestas adecuadas: Utilizar los códigos de estado HTTP correctos para reflejar el resultado de la operación. Por ejemplo, 204 No Content cuando la eliminación es exitosa y no se devuelve contenido en el cuerpo de la respuesta.
Gestión de errores: Proporcionar mensajes de error claros y manejar excepciones de forma que no se revele información sensible sobre el sistema.
Registro y monitorización: Implementar mecanismos de logging para registrar las operaciones de eliminación y facilitar la monitorización y auditoría de las acciones realizadas.
Extensión a múltiples eliminaciones
En casos donde se requiera eliminar múltiples recursos en una sola solicitud, se puede implementar una ruta que acepte un cuerpo con la lista de identificadores:
@Bean
public RouterFunction<ServerResponse> routes(Handler handler) {
return RouterFunctions.route()
.DELETE("/api/funcional/usuarios", handler::eliminarUsuarios)
.build();
}
El handler correspondiente manejaría la lista de identificadores:
public Mono<ServerResponse> eliminarUsuarios(ServerRequest request) {
Mono<List<String>> idsMono = request.bodyToMono(new ParameterizedTypeReference<List<String>>() {});
return idsMono.flatMapMany(Flux::fromIterable)
.flatMap(id -> usuarioService.eliminarUsuario(id))
.then(ServerResponse.noContent().build())
.onErrorResume(error -> ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue("Error al eliminar usuarios: " + error.getMessage()));
}
En este ejemplo:
- Se convierte el cuerpo de la solicitud en un
Mono<List<String>>
que contiene los identificadores. - Se utiliza
flatMapMany
para convertir elMono
en unFlux<String>
y procesar cadaid
individualmente. - Se llama a
usuarioService.eliminarUsuario(id)
para eliminar cada usuario. - Tras completar todas las eliminaciones, se devuelve una respuesta 204 No Content.
Consideraciones sobre la concurrencia
En entornos reactivos, es crucial tener en cuenta la concurrencia y la consistencia de los datos. Al eliminar recursos, especialmente en operaciones simultáneas, pueden surgir condiciones de carrera. Es aconsejable implementar mecanismos de control como bloqueos optimistas o pesimistas si la lógica de negocio lo requiere.
Además, es importante que las operaciones en la capa de datos sean atómicas y transaccionales cuando sea necesario, para mantener la integridad de los datos.
Integración con pruebas unitarias
Para asegurar el correcto funcionamiento de los handlers DELETE, se deben escribir pruebas unitarias que validen los diferentes escenarios:
- Eliminación exitosa de un usuario existente.
- Intento de eliminar un usuario que no existe.
- Gestión de errores en caso de fallos en la capa de datos.
- Verificación de que las respuestas HTTP tienen los códigos de estado correctos.
Las pruebas se pueden implementar utilizando WebTestClient
para simular solicitudes HTTP y verificar las respuestas:
@SpringBootTest
@AutoConfigureWebTestClient
public class HandlerTests {
@Autowired
private WebTestClient webTestClient;
@Test
public void testEliminarUsuarioExistente() {
webTestClient.delete()
.uri("/api/funcional/usuarios/{id}", "123")
.exchange()
.expectStatus().isNoContent();
}
@Test
public void testEliminarUsuarioNoExistente() {
webTestClient.delete()
.uri("/api/funcional/usuarios/{id}", "999")
.exchange()
.expectStatus().isNotFound();
}
}
Con estas pruebas, se valida que el handler responde adecuadamente en diferentes situaciones, asegurando la calidad y fiabilidad del servicio.
La implementación de métodos DELETE usando handlers funcionales en servicio con Spring WebFlux permite aprovechar las ventajas de la programación reactiva y funcional, ofreciendo una manera eficiente y elegante de gestionar operaciones de eliminación de recursos. Al seguir las prácticas recomendadas y prestar atención a los detalles, se pueden construir servicios robustos y escalables que cumplan con los estándares modernos de desarrollo.
Ejercicios de esta lección Controlador REST reactivo funcional
Evalúa tus conocimientos de esta lección Controlador REST reactivo funcional 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
- Crear un API REST reactivo con Spring WebFlux
- Utilizar controladores funcionales en lugar de basados en anotaciones
- Aprender las clases de rutas y handlers funcionales
- Utilizar operadores reactivos