¿Qué es @ControllerAdvice?

Anotación de Spring Boot @ControllerAdvice se introduzco a partir de la versión 3.2 en adelante (con 3.2 nos realmente nos referimos a 1.3.2). Podemos ver la versión de Spring Boot que estamos utilizando en el momento de levantar nuestra aplicación de Spring Boot desde la consola/terminal. En este caso en particular, podemos observar que estoy trabajando con la versión 2.2.2 (si tenéis una versión diferente no os preocupéis siempre que sea superior a la 1.3.2 ya que sino no sería compatible con @ControllerAdvice).

La anotación @ControllerAdvice nos permite manejar las excepciones (handler exceptions) de nuestra aplicación. Su peculiaridad es que esta anotación no maneja las excepciones por ejemplo de un controlador en específico, sino que manejará/capturará todas las excepciones de toda nuestra aplicación. Vamos a ver un ejemplo.

Creando el proyecto

Creamos un proyecto y le añadimos las dependencias:

La estructura del proyecto será:

Time.java

Vamos a crear una clase estática para no tener que realizar el new cada vez que la usemos.

Response.java

Si lo hacemos con la manera tradicional (con Getters&Setters y constructores), es decir, sin Lombok.

O la con Lombok (la más elegante y por tanto la manera más aconsejada), si no lo conocéis os aconsejo primeramente mirar el artículo: Introducción a Lombok donde preparamos el IDE y explicamos las diferentes anotaciones que tenemos.

En este caso en particular, usaremos la anotación @Data (que si hacemos memoria sería equivalente a definir: @Setter, @Getter, @RequiredArgsConstructor, @ToString y @EqualsAndHahsCode) y finalmente añadiremos también los constructor con @NoArgsConstructor y @AllArgsConstructor.

Y si vamos a Window>Show View > Outline podemos ver que internamente y aunque no están visibles desde dentro la de la clase, tenemos los Getters & Setters y constructores correctamente creados:

AnimalController.java

Si analizamos el siguiente controlador podemos ver que hemos definido un método llamado getAnimals() en el que tenemos 3 animales definidos dentro de un lista de Strings. En este caso en particular, los hemos realizado así para no tener que quitar el foco del protagonista de este artículo que es el manejo de errores. Aunque lo más común y lo ideal como ya hemos visto en el artículo CRUD REST con Spring Boot y JPA sería que los animales vinieran de la capa de repositorio. Pueden existir muchos factores que provoquen un error en la petición al repositorio y aunque nosotros ya hemos trabajado anteriormente con opcionales, estos cubrían en parte todo esto, lo aconsejable es proteger más aún el código además de tener un control sobre él porque pasa algo y tener un poco más de contexto para que sea más fácil detectar porque se produce un error. Todo esto, también os aseguro que nos ayudará a dormir un poco más tranquilos por la noche.

Si mostramos todos los animales (método getAnimals) vemos que no no hay problema alguno, nos muestra el listado porque está lleno.

O desde Postman:

Pero si os planteo el escenario de ¿Qué pasaría si la lista estuviera vacía? Pues en esa situación por el momento nos salvamos del error.

En cambio, si ejecutamos el método getAnimalById y mantenemos el listado vacío o intentamos acceder a una posición del array que no esté previamente definida. ¿Qué pasará?

Tras la ejecución podemos observar cómo se queja en el Terminal:

¿Qué vamos a hacer para manejar las excepciones?

Lo que vamos a hacer es manejar excepciones es:

  • Manejar/capturar excepciones sobre un controlador en específico: en nuestro caso manejaremos las excepciones del tipo IndexOutOfBoundsExceptions que se produzcan únicamente sobre nuestro controlador AnimalsController. El manejo de las excepciones lo realizaremos mediante al uso de la anotación @ExceptionHandler sobre el que le detallaremos que capture las que sean de tipo IndexOutOfBoundsExceptions. Este tipo de excepciones se producirán al intentar acceder a un elemento del array que no existe. Un ejemplo del esquema hasta el momento podría ser:
  • Manejar/capturar del resto de excepciones mediante a una clase genérica: aunque aún la tenemos que definir. En nuestro caso la clase de llamará ExceptionGlobalResponse y contendrá la anotación @ControllerAdvice. Esta anotación, nos permitirá recibir todas las Excepciones que no capture la clase anterior (AnimalsController). Y si tenemos el @ExceptionHandler que capture esa excepción podremos manejarla a nuestro gusto. Un ejemplo del esquema completo podría ser:

Manejando Excepciones especificas con @ExceptionHanler para un controlador en específico (AnimalController)

La anotación @ExceptionHanler, nos permite manejar las Excepciones a partir de un método desde dentro de una clase. Por el momento vamos a manejar únicamente y desde y para nuestro controlador AnimalController las Exceptions de tipo IndexOutOfBoundsException. De esta manera, cada vez que se llame a getAnimalById y no exista el elemento al producirse una excepción de tipo IndexOutOfNoundsException esta excepción será capturada/manejada por el propio controlador AnimalController.

Si vemos este esquema de las excepciones nos puede ayudar para situarnos correctamente:

Si ahora arrancamos la aplicación de nuevo e intentamos realizar una petición sobre el elemento 5 por ejemplo, podemos observar que nos muestra:

En el método getAnimalById de la clase AnimalController hemos dejado preparada la generación de una Excepción cuando el parámetro que se introduzca sea 10. Si la testeamos podemos ver que vuelve a petar:

Manejando Excepciones globales con @ExceptionHanler y @ControllerAdvice (ExceptionGlobalRespons)

Para solucionar esto vamos a generar un Controller que capture todas las excepciones de tipo RunTime y Exception. Si no tenemos la excepción capturada dentro del AnimalController, al no capturar la excepción, ya hemos visto el resultado.

Para solucionar esto usaremos la anotación @ControllerAdvice lo que hará Spring Boot es preguntarse ¿La clase captura esta excepción? Si la respuesta es si perfecto, si la respuesta es no lo mandará a la clase con la anotación @ControllerAdvice. Por tanto, lo que hará está clase es intentar capturar todas las excepciones que no se hayan capturado desde la clase donde se produzcan.

Vamos a ver un ejemplo:

Cada vez que se produzca un error, al tener declarados dentro del ExceptionGlobalResponse los métodos en el siguiente orden: primeramente manejarán los RunTimeExcepcion y posteriormente los que sean de tipo Exception.

La captura de estas excepciones se realizará a través de las anotaciones de mapeo como: @RequestMaping o las anotaciones «hijas» que salen de este elemento padre como son por ejemplo: @GetMapping, @PostMapping, @PutMapping, @DeleteMapping y @PatchMapping.

La anotación @ControllerAdvice se aplicará automáticamente a todas las clases que utilicen la anotación @Controller (anotación especifica de la anotación padre @Component) y/o @RestController (que extiende de @Controller).

Protegiendo tu aplicación de fallos externos concepto de Circuit Breaker (Cortocircuito)

Si hacemos pruebas sin proteger los errores (como ya hemos visto un poco más arriba) y no capturamos la excepción, pese a que ha producido una excepción podremos continuar lanzando peticiones. El flujo de ejecución no se detendrá y está nos responderá con normalidad. ¿A qué es debido esto? Esto se produce debido a que si se genera un fallo interno dentro de nuestra aplicación, que depende de nuestra aplicación (algo interno) y Spring Boot suele tener mecanismos para auto protegerse. Por lo tanto, en los errores internos no solemos tener problemas, pese a ello, mejor siempre protegernos. Si comparáramos esto con un de un contador de la luz sería como si Spring Boot bajará el plomo e inmediatamente lo subiera, pero a una velocidad imperceptible para nosotros. Y que solamente nos permite verlo por la consola y por el navegador al realizar la petición (siempre que no estemos hablando de un fallo muy muy muy grave que Spring Boot no pudiera salvar) y como hemos dicho no afectaría al funcionamiento (como norma general).

Pero ¿Qué pasa si el servicio no depende de Spring Boot? Y por ejemplo nos estamos intentando conectar a un servicio externo como puede ser: BBDD, servicios de otras compañías, APIs, etc. Aquí es donde está el problema, y os aconsejo que lo metáis en un try catch o bien que manejéis las excepciones o sino que os preparéis para algo similar al fuego de la siguiente imagen.

Ya que si por ejemplo, se pueden producir situaciones como:

  • Se corta la comunicación, sin restablecerte rápidamente antes de que se acabe el tiempo definido (time out) del protocolo TCP.
  • Se produce un pico de peticiones y no tenemos la suficiente escalabilidad para responder a estas peticiones el tiempo de respuesta se va aumentado hasta superar el tiempo definido (time out) del protocolo TCP.

La comunicación entre servicios externos se suele establecer mediante al protocolo de comunicación TCP (Transmission Control Protocol). Usualmente, suele tener establecido 30 segundos como configuración por defecto.

«Degrade elegante» o gracefully defradation

Si está conexión supera el tiempo establecido sin recibir respuesta alguna, se generará un error, este error será controlado/manejado mediante al manejo de excepciones que hemos visto anteriormente en este artículo. Gracias al cual podremos continuar respondiendo con mensajes por pantalla y no con errores mensajes de logger en un fichero o por consola.

El concepto de continuar respondiendo pese a que se produzca un error se conoce bajo el nombre de «degrade elegante» o gracefully defradation.

Espero que os haya gustado el artículo ¡Un saludo javer@s!