Manejo de excepciones en Spring Boot

Motivación

Se requiere un mecanismo global o centralizado basado en las excepciones, para poder manejar y traducir en errores entendibles hacia las integraciones (frontend y/o otros clientes).

Introducción

La gestión de excepciones en el sentido amplio de la palabra, se refiere a la captura de eventos de tipo errores que se producen en las aplicaciones durante su ejecución. Estos errores pueden ser de muchos tipos, desde fallas de red, errores en accesos a una base de datos, errores en tiempo de ejecución (runtime) o simplemente por validaciones de negocio.

El manejo de excepciones tiene como objetivo hacer más seguras y robustas las aplicaciones así como también tener un mecanismo para dar una respuesta elegante a los usuarios mejorando la confiabilidad y resistencia a fallas en las aplicaciones.

Usos

  • Manejo global de excepciones:

    • ¿ Cuándo usarlo ?
      • Cuando se necesita un formato de respuesta de error consistente para toda la aplicación.
      • Para evitar exponer trazas de la aplicación con información relevante, de esta forma evitar posibles futuros atacantes.
  • Manejo de excepciones en el controlador:

    • Utilizar cuando se necesita una solución particular (distinta a la ofrecida por el manejo de errores globales), como por ejemplo, integraciones donde necesitamos adaptar nuestra respuesta.

Implementación

Para su implementación, Spring Boot nos provee algunas herramientas, las más comunes para este tipo de manejadores son @ControllerAdvice y de @RestControllerAdvice, el primero para cualquier tipo de controlador anotado con @Controller (MCV de Spring) y el segundo esta especializado en controladores de tipo Rest (aplica sólo a los controladores anotados como @RestController)

Para que estos componentes puedan funcionar correctamente y como su nombre lo indica, deben poder atrapar excepciones del tipo específico (jerarquía) que necesitemos manejar y hacer la interpretación de la excepciín, como por ejemplo: se requiere atrapar cualquier Exception del tipo IllegalArgumentException y convertirla en un bad request (error 400 de http).

Hablemos un poco de excepciones

Los manejadores de excepciones siempre actúan cuando una excepción es lanzada y no ha sido atrapada por la capa de controladores, éstas pueden ser lanzadas desde las capas de acceso a datos, directamente desde la capa de negocio o simplemente desde la capa de controladores (validación de entrada).

Aquí la discusión se vuelve un poco densa y depende mucho el cómo quieres trabajar tu jerarquía de excepciones, puede ser tan compleja como quieras (casos en que necesitas mucho detalle en la respuesta del manejador) o tan simple como mantener un par de excepciones que manejar e interpretar.

En un articulo anterior escribí acerca de tipos de excepciones, jerarquía y características el manejo de excepciones. Les dejo aquí el enlace a este artículo Checked y Unchecked Exception.

Una de las alternativas mas simples es utilizar Excepciones de tipo RunTimeException ya que tienen la característica de no ser atrapadas de forma imperativa o declarativa, de esta forma excepciones que son lanzadas en las capas inferiores saltan hasta el manejador de Excepciones, claro está, solo si son manejadas en dicho manejador.

Veamos un ejemplo

En SpringBoot tenemos un par de mecanismos para poder manejar excepciones y que nos permite evitar exponer el stack trace directo al usuario (nos da seguridad en caso de exponer algún dato sensible) y en su reemplazo nos permite modelar una salida elegante dependiendo de la excepción lanzada.

Para efectos prácticos en este artículo utilizaremos sólo excepciones de tipo Runtime ya que este tipo de excepciones suben por el stack sin ser capturadas por nuestro código hasta llegar al lugar donde queremos hacer el manejo de la excepción.

Rest Controller Advice

GlobalExceptionHandleres una clase que se anota con @RestControllerAdvice y es donde podremos hacer el manejode la excepción. Veamos un ejemplo a continuación:

En este primer ejemplo, se evidencia que se atrapan las exepciones especificas y mas altas de la jerarquía, NotFoundException, BadRequestException y UnauthorizedException, ademas para cada una de ellas hay un método disponible donde podremos por ejemplo hacer un log del error para tener el detalle de lo ocurrido y finalmente enviar hacia el consumnidor, el correspondiente codigo Http para cada caso. Nota: en este ejemplo el body de la respuesta es vacío.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package cl.pcollaog.eh.handler;

import cl.pcollaog.eh.exception.BadRequestException;
import cl.pcollaog.eh.exception.NotFoundException;
import cl.pcollaog.eh.exception.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<Void> badRequestHandler(BadRequestException bre) {
LOGGER.error("badRequestHandler - message {}", bre.getMessage());
return ResponseEntity.badRequest().build();
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Void> notFoundExceptionHandler(NotFoundException nfe) {
LOGGER.error("notFoundExceptionHandler - message: {}", nfe.getMessage());
return ResponseEntity.notFound().build();
}

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<Void> unauthorizedExceptionHandler(UnauthorizedException ue) {
LOGGER.error("unauthorizedExceptionHandler - message: {}", ue.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}

En este segundo ejemplo, vamos a manipular la respuesta (vamos a suponer que la integración lo pide así) para cada caso.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package cl.pcollaog.eh.handler;

import cl.pcollaog.eh.exception.BadRequestException;
import cl.pcollaog.eh.exception.NotFoundException;
import cl.pcollaog.eh.exception.UnauthorizedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.time.LocalDateTime;

@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(BadRequestException.class)
public ResponseEntity<CustomResponse> badRequestHandler(BadRequestException bre) {
LOGGER.error("badRequestHandler - message {}", bre.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.BAD_REQUEST), bre.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

@ExceptionHandler(NotFoundException.class)
public ResponseEntity<CustomResponse> notFoundExceptionHandler(NotFoundException nfe) {
LOGGER.error("notFoundExceptionHandler - message: {}", nfe.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.NOT_FOUND), nfe.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<CustomResponse> unauthorizedExceptionHandler(UnauthorizedException ue) {
LOGGER.error("unauthorizedExceptionHandler - message: {}", ue.getMessage());
CustomResponse cr = new CustomResponse((HttpStatus.UNAUTHORIZED), ue.getMessage());
return new ResponseEntity<>(cr, cr.getStatus());
}

public static class CustomResponse {

private final LocalDateTime date;

private final HttpStatus status;

private final String message;

public CustomResponse(HttpStatus status, String message) {
this.date = LocalDateTime.now();
this.status = status;
this.message = message;
}
// ... Omitiré los getter
}
}

Para el caso de NotFoundException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 404 
Connection: keep-alive
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:20:02 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:20:02.462356",
"message": "Entity not found",
"status": "NOT_FOUND"
}

Para el caso de BadRequestException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
HTTP/1.1 400 
Connection: close
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:16:41 GMT
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:16:41.886069",
"message": "Invalid argument",
"status": "BAD_REQUEST"
}

Finalmente para el caso de UnauthorizedException la respuesta será la siguiente:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 401 
Connection: keep-alive
Content-Type: application/json
Date: Sat, 16 Mar 2024 22:21:38 GMT
Keep-Alive: timeout=60
Transfer-Encoding: chunked

{
"date": "2024-03-16T19:21:38.011316",
"message": "Invalid Token",
"status": "UNAUTHORIZED"
}

Como podrán ver, en el handler (método) podrán manejar la excepción a conveniencia y podrán enviar el mensaje y codigo de error que necesiten hacia el consumidor del servicio.

Para finalizar les dejo un diagrama del funcionamiento del GlobalExceptionHandler con el ejemplo de 404 Not Found.

Si tienen alguna observación no duden en comentar.

Author

Francisco Collao

Posted on

2024-03-16

Updated on

2025-10-28

Licensed under

Comentarios