Manejo de excepciones en Spring

Introducción

En este artículo, veremos algunos enfoques del manejo de excepciones en las aplicaciones REST de Spring.
Este tutorial asume que usted tiene un conocimiento básico de Spring y puede crear API REST simples al usarlo.
Si desea leer más acerca de las excepciones y las excepciones personalizadas en Java, lo hemos cubierto en detalle en Manejo de excepciones en Java: una guía completa con las mejores y las 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
Vamos a crear un controlador REST con un mapeo que espere idy devuelva Usercon el dado idsi 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 solo encontrar al usuario, también tenemos que realizar verificaciones adicionales, ya que el idque se aprobó siempre debe ser mayor que 0, de lo contrario, debemos devolver un BAD_REQUESTcódigo de estado.
Del mismo modo, si no se encuentra al usuario, tenemos que devolver un NOT_FOUNDcódigo de estado. Además, es posible que tengamos que agregar texto para algunos detalles sobre el error al cliente.
Para cada verificación, tenemos que crear un objeto ResponseEntity que tenga códigos de respuesta y texto de acuerdo con nuestros requisitos.
Podemos ver fácilmente que estas comprobaciones se realizarán 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 ResponseEntityobjetos. Esto crea el problema de mantener las consistencias dentro de la aplicación.
Entonces, el problema que estamos tratando de resolver es la separación de las preocupaciones. Por supuesto, tenemos que realizar estas comprobaciones en cada uno, RequestMappingpero en lugar de manejar los 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 violación y estas excepciones se manejarán por separado.
Ahora, puede usar las excepciones incorporadas ya proporcionadas por Java y Spring , o 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 los mensajes de error del servidor predeterminado al cliente cuando se entrega una API. Tampoco podemos devolver trazas de pila que son complicadas y difíciles de entender para nuestros clientes. El manejo adecuado de las excepciones con Spring es un aspecto muy importante de la construcción de una buena API REST.
A lo largo del manejo de excepciones, la documentación de REST API es una necesidad.

Manejo de excepciones a través de @ResponseStatus

La anotación @ResponseStatus se puede usar 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.
Vamos a crear una excepción personalizada para manejar la situación cuando el usuario no se encuentra. Esta será una excepción de tiempo de ejecución, por lo tanto, tenemos que extender la java.lang.RuntimeExceptionclase.
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, utiliza la configuración provista 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 está más limpio con la separación de preocupaciones.

@RestControllerAdvice y @ExceptionHandler

Vamos a crear una excepción personalizada para manejar las verificaciones de validación. Esto nuevamente 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ódigos comunes para el manejo de excepciones.
Esto generalmente se usa junto con @ExceptionHandlerque 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());
    }
}
Puedes considerarlo RestControllerAdvicecomo una especie de Aspecto en tu código Spring. Siempre que su código Spring emita una excepción que tenga un controlador definido en esta clase, la lógica apropiada podría escribirse de acuerdo con las necesidades del negocio.
Tenga en cuenta que a diferencia de @ResponseStatusesto, podríamos hacer muchas cosas con este enfoque, como registrar nuestras excepciones, notificar, etc.
¿Qué pasaría si quisiéramos actualizar la edad de un usuario existente? Tenemos 2 verificaciones de validación que deben hacerse:
  • El iddebe ser mayor que 0
  • El agedebe estar entre 20 y 60.
Con eso en mente, hagamos un punto final 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);
    }
De forma predeterminada, @RestControllerAdvicees 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 puedes hacer algo como:
@RestControllerAdvice(basePackages = "my.package")
o
@RestControllerAdvice(basePackageClasses = MyController.class)
Para aplicar a la 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 ampliar esta clase y anular los 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 @ResponseControllerAdvice.
Una vez más, 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 hacer el manejo de excepciones en nuestras aplicaciones. Puede usar uno o una combinación de ellos según sus necesidades. Aquí está la regla de oro:
  • Para las excepciones personalizadas donde su código de estado y su mensaje son fijos, considere agregarlos @ResponseStatus.
  • Para excepciones donde necesita hacer algún registro, use @RestControllerAdvicecon @ExceptionHandlerTambién tiene más controlador sobre el texto de respuesta aquí.
  • Para cambiar el comportamiento de las respuestas de excepción predeterminadas de Spring, puede extender la ResponseEntityExceptionHandlerclase.
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 para los ejemplos utilizados en este artículo se puede encontrar en Github .

Acerca de: Programator

Somos Instinto Programador

0 comentarios:

Publicar un comentario

Dejanos tu comentario para seguir mejorando!

Con tecnología de Blogger.