Manejo centralizado de excepciones en controladores Spring Boot

En aplicaciones Spring Boot, es una buena práctica centralizar la gestión de excepciones que se producen en los controladores REST. Existen principalmente dos enfoques para lograr esto:

  1. Enfoque declarativo: mediante @RestControllerAdvice junto con @ExceptionHandler.
  2. Anfoque mediante AOP: utilizando programación orientada a aspectos con @Aspect.

Enfoque declarativo con @RestControllerAdvice

La anotación @RestControllerAdvice permite definir un componente global que intercepta las excepciones lanzadas desde cualquier método anotado con @RequestMapping o sus variantes (@GetMapping, @PostMapping, etc.). Combinado con @ExceptionHandler, se puede manejar cada tipo de excepción de forma específica.

A continuación, se muestra un ejemplo de implementación:


@RestControllerAdvice
public class GlobalExceptionHandler {

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

    @ExceptionHandler(Exception.class)
    public ApiResponse handleException(HttpServletRequest request, HttpServletResponse response, Exception ex) {
        log.error("Se produjo una excepción en la petición: {}", request.getRequestURI(), ex);

        if (response.getStatus() == HttpStatus.BAD_REQUEST.value()) {
            log.info("Ajustando el código de estado HTTP a 200");
            response.setStatus(HttpStatus.OK.value());
        }

        if (ex instanceof NullPointerException) {
            log.error("Error NPE: {}", ex.getMessage());
            return ApiResponse.error("Se ha producido una referencia nula");
        }

        if (ex instanceof IllegalArgumentException) {
            log.error("Error de argumentos: {}", ex.getMessage());
            return ApiResponse.error("Los parámetros de la petición no son válidos");
        }

        if (ex instanceof SQLException) {
            log.error("Error SQL: {}", ex.getMessage());
            return ApiResponse.error("Error al acceder a la base de datos");
        }

        log.error("Error no controlado: {}", ex.getMessage());
        return ApiResponse.error("Error interno del servidor, contacte al administrador");
    }
}

En este ejemplo, ApiResponse es una clase contenedora que estandariza el formato de las respuestas. Dependiendo del tipo de excepción capturada, se devuelve un mensaje apropiado al cliente.

Anfoque con AOP

El uso de AOP permite interceptar las llamadas a los métodos del controlador y envolver su ejecución en un bloque try-catch. De esta manera, cualquier excepción no capturada dentro del controlador será interceptada por el aspecto.

A continuación, se presenta un ejemplo utilizando @Aspect con un pointcut que abarca todos los métodos de las clases dentro del paquete controller:


@Aspect
@Component
public class ControllerExceptionAspect {

    private static final Logger log = LoggerFactory.getLogger(ControllerExceptionAspect.class);

    @Around("execution(public com.example.model.ApiResponse com..*.controller..*.*(..))")
    public Object interceptControllerMethod(ProceedingJoinPoint joinPoint) {
        long startTime = System.currentTimeMillis();

        ApiResponse> apiResponse;

        try {
            log.info("Inicio de ejecución del método: {} con argumentos: {}",
                    joinPoint.getSignature(), Arrays.toString(joinPoint.getArgs()));

            apiResponse = (ApiResponse>) joinPoint.proceed(joinPoint.getArgs());

            long elapsed = System.currentTimeMillis() - startTime;
            log.info("Fin de ejecución del método: {} en {} ms", joinPoint.getSignature(), elapsed);

        } catch (Throwable ex) {
            apiResponse = processException(joinPoint, ex);
        }

        return apiResponse;
    }

    private ApiResponse> processException(ProceedingJoinPoint joinPoint, Throwable ex) {
        log.error("Excepción capturada en el método: {} con argumentos: {}. Causa: {}",
                joinPoint.getSignature(),
                Arrays.toString(joinPoint.getArgs()),
                ex.getMessage(),
                ex);

        if (ex instanceof RuntimeException) {
            return ApiResponse.error(ex.getMessage());
        }

        return ApiResponse.error("Ocurrió un error inesperado durante la ejecución");
    }
}

En este caso, el pointcut execution(public com.example.model.ApiResponse com..*.controller..*.*(..)) intercepta todos los métodos públicos dentro de cualquier subpaquete controller que devuelvan un objeto de tipo ApiResponse.

Publicado el 8-9 18:13