Manejo de excepciones en Spring

    Introducción

    En este artículo, analizaremos algunos enfoques de manejo de excepciones en aplicaciones Spring REST.

    Este tutorial asume que tiene un conocimiento básico de Spring y puede crear API REST simples usándolo.

    Si desea leer más sobre las excepciones y las excepciones personalizadas en Java, lo cubrimos en detalle en Manejo de excepciones en Java: una guía completa con las mejores y peores prácticas y cómo hacer excepciones personalizadas en Java.

    ¿Por que hacerlo?

    Supongamos que tenemos un servicio de usuario simple donde podemos buscar y actualizar usuarios registrados. Tenemos un modelo simple definido para los usuarios:

    public class User {
        private int id;
        private String name;
        private int age;
    
        // Constructors, getters, and setters
    

    Creemos un controlador REST con un mapeo que espera un id y devuelve el User con lo dado id si está presente:

    @RestController
    public class UserController {
    
        private static List<User> userList = new ArrayList<>();
        static {
            userList.add(new User(1, "John", 24));
            userList.add(new User(2, "Jane", 22));
            userList.add(new User(3, "Max", 27));
        }
    
        @GetMapping(value = "/user/{id}")
        public ResponseEntity<?> getUser(@PathVariable int id) {
            if (id < 0) {
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
            }
            User user = findUser(id);
            if (user == null) {
                return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
            }
    
            return ResponseEntity.ok(user);
        }
    
        private User findUser(int id) {
            return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElse(null);
        }
    }
    

    Además de simplemente encontrar al usuario, también tenemos que realizar comprobaciones adicionales, como la id que se pasa siempre debe ser mayor que 0; de lo contrario, tenemos que devolver un BAD_REQUEST código de estado.

    Del mismo modo, si no se encuentra el usuario, tenemos que devolver un NOT_FOUND código de estado. Además, es posible que tengamos que agregar texto para algunos detalles sobre el error al cliente.

    Para cada cheque, tenemos que crear un ResponseEntity objeto que tiene códigos de respuesta y texto de acuerdo con nuestros requisitos.

    Podemos ver fácilmente que estas comprobaciones deberán realizarse varias veces a medida que crezcan nuestras API. Por ejemplo, supongamos que estamos agregando un nuevo mapeo de solicitud de PATCH para actualizar a nuestros usuarios, necesitamos crear nuevamente estos ResponseEntity objetos. Esto crea el problema de mantener la coherencia dentro de la aplicación.

    Entonces, el problema que estamos tratando de resolver es la separación de preocupaciones. Por supuesto, tenemos que realizar estas comprobaciones en cada RequestMapping pero en lugar de manejar escenarios de validación / error y qué respuestas deben devolverse en cada uno de ellos, simplemente podemos lanzar una excepción después de una infracción y estas excepciones se manejarán por separado.

    Ahora, puede utilizar las excepciones integradas que ya proporciona Java y Springo, si es necesario, puede crear sus propias excepciones y lanzarlas. Esto también centralizará nuestra lógica de validación / manejo de errores.

    Además, no podemos devolver mensajes de error del servidor predeterminados al cliente cuando se sirve una API. Tampoco podemos devolver trazas de pila que sean complicadas y difíciles de entender para nuestros clientes. El manejo adecuado de excepciones con Spring es un aspecto muy importante de la construcción de una buena API REST.

    Junto con el manejo de excepciones, la documentación de la API REST es imprescindible.

    Manejo de excepciones a través de @ResponseStatus

    los @ResponseStatus La anotación se puede utilizar en métodos y clases de excepción. Se puede configurar con un código de estado que se aplicaría a la respuesta HTTP.

    Creemos una excepción personalizada para manejar la situación cuando no se encuentra al usuario. Esta será una excepción de tiempo de ejecución, por lo tanto, tenemos que extender el java.lang.RuntimeException clase.

    También marcaremos esta clase con @ResponseStatus:

    @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "User Not found")
    public class UserNotFoundException extends RuntimeException {
    
    }
    

    Cuando Spring detecta esta excepción, usa la configuración proporcionada en @ResponseStatus.

    Cambiando nuestro controlador para usar el mismo:

        @GetMapping(value = "/user/{id}")
        public ResponseEntity<?> getUser(@PathVariable int id) {
            if (id < 0) {
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
            }
            User user = findUser(id);
            return ResponseEntity.ok(user);
        }
    
        private User findUser(int id) {
            return userList.stream().filter(user -> user.getId().equals(id)).findFirst().orElseThrow(() -> new UserNotFoundException());
        }
    

    Como podemos ver, el código ahora es más limpio con separación de preocupaciones.

    @RestControllerAdvice y @ExceptionHandler

    Creemos una excepción personalizada para manejar las comprobaciones de validación. Esto de nuevo será un RuntimeException:

    public class ValidationException extends RuntimeException {
        private static final long serialVersionUID = 1L;
        private String msg;
    
        public ValidationException(String msg) {
            this.msg = msg;
        }
    
        public String getMsg() {
            return msg;
        }
    }
    

    @RestControllerAdvice es una nueva característica de Spring que se puede usar para escribir código común para el manejo de excepciones.

    Esto generalmente se usa junto con @ExceptionHandler que en realidad maneja diferentes excepciones:

    @RestControllerAdvice
    public class AppExceptionHandler {
    
        @ResponseBody
        @ExceptionHandler(value = ValidationException.class)
        public ResponseEntity<?> handleException(ValidationException exception) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMsg());
        }
    }
    

    Tu puedes pensar en RestControllerAdvice como una especie de Aspecto en su código de Spring. Siempre que su código Spring arroje una excepción que tenga un controlador definido en esta clase, se podría escribir la lógica adecuada de acuerdo con las necesidades comerciales.

    Note que a diferencia de @ResponseStatus podríamos hacer muchas cosas con este enfoque, como registrar nuestras excepciones, notificar, etc.

    ¿Y si quisiéramos actualizar la edad de un usuario existente? Tenemos 2 controles de validación que deben realizarse:

    • los id debe ser mayor que 0
    • los age debe tener entre 20 y 60

    Con eso en mente, creemos un punto final solo para eso:

        @PatchMapping(value = "/user/{id}")
        public ResponseEntity<?> updateAge(@PathVariable int id, @RequestParam int age) {
            if (id < 0) {
                throw new ValidationException("Id cannot be less than 0");
            }
            if (age < 20 || age > 60) {
                throw new ValidationException("Age must be between 20 to 60");
            }
            User user = findUser(id);
            user.setAge(age);
    
            return ResponseEntity.accepted().body(user);
        }
    

    Por defecto @RestControllerAdvice es aplicable a toda la aplicación, pero puede restringirla a un paquete, clase o anotación específicos.

    Para la restricción de nivel de paquete, puede hacer algo como:

    @RestControllerAdvice(basePackages = "my.package")
    

    o

    @RestControllerAdvice(basePackageClasses = MyController.class)
    

    Para aplicar a una clase específica:

    @RestControllerAdvice(assignableTypes = MyController.class)
    

    Para aplicarlo a controladores con ciertas anotaciones:

    @RestControllerAdvice(annotations = RestController.class)
    

    ResponseEntityExceptionHandler

    ResponseEntityExceptionHandler proporciona un manejo básico para las excepciones de Spring.

    Podemos extender esta clase y anular métodos para personalizarlos:

    @RestControllerAdvice
    public class GlobalResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
    
        @Override
        protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
            return errorResponse(HttpStatus.BAD_REQUEST, "Required request params missing");
        }
    
        private ResponseEntity<Object> errorResponse(HttpStatus status, String message) {
            return ResponseEntity.status(status).body(message);
        }
    }
    

    Para registrar esta clase para el manejo de excepciones, tenemos que anotarla con @ResponseControllerAdvice.

    De nuevo, hay muchas cosas que se pueden hacer aquí y depende de sus requisitos.

    ¿Cuál usar cuando?

    Como puede ver, Spring nos brinda diferentes opciones para manejar excepciones en nuestras aplicaciones. Puede utilizar uno o una combinación de ellos según sus necesidades. Aquí está la regla de oro:

    • Para las excepciones personalizadas en las que su código de estado y su mensaje son fijos, considere agregar @ResponseStatus a ellos.
    • Para las excepciones en las que necesita hacer algunos registros, use @RestControllerAdvice con @ExceptionHandler. También tienes más control sobre tu texto de respuesta aquí.
    • Para cambiar el comportamiento de las respuestas de excepción de Spring predeterminadas, puede extender el ResponseEntityExceptionHandler clase.

    Nota: Tenga cuidado al mezclar estas opciones en la misma aplicación. Si se maneja lo mismo en más de un lugar, es posible que obtenga un comportamiento diferente al esperado.

    Conclusión

    En este tutorial, discutimos varias formas de implementar un mecanismo de manejo de excepciones para una API REST en Spring.

    Como siempre, el código de los ejemplos utilizados en este artículo se puede encontrar en Github.

    Etiquetas:

    Deja una respuesta

    Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *