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 *