Manejo de excepciones en Spring

En este artículo, veremos algunos enfoques de manejo de excepciones en las aplicaciones Spring REST. Este tutorial asume que usted tiene un conocimiento básico de...

Introducción

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

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

If you'd like to read more about exceptions and custom exceptions in Java, we've covered it in detail in Manejo de excepciones en Java: una guía completa con las mejores y peores prácticas and Cómo hacer excepciones personalizadas en Java.

¿Por que hacerlo? {#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:

1
2
3
4
5
6
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 espera un id y devuelve el Usuario con el id dado si está presente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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 encontrar al usuario, también tenemos que realizar comprobaciones adicionales, como que el id que se pasa siempre debe ser mayor que 0, de lo contrario, tenemos que devolver un código de estado BAD_REQUEST.

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

Para cada verificación, debemos crear un objeto Entidad de respuesta que tenga respuesta códigos y texto de acuerdo a nuestros requerimientos.

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 PATCH para actualizar a nuestros usuarios, necesitamos crear nuevamente estos objetos ResponseEntity. 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 luego se manejarán. por separado.

Ahora, puede utilizar las excepciones integradas que ya proporcionan Java y Primavera 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 mensajes de error del servidor predeterminados al cliente al servir una API. Tampoco podemos devolver rastros de pila que sean intrincados y difíciles de entender para nuestros clientes. El manejo adecuado de excepciones con Spring es un aspecto muy importante para construir una buena API REST.

Alongisde exception handling, Documentación de la API REST is a must.

Gestión de excepciones mediante @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 no se encuentra el usuario. Esta será una excepción de tiempo de ejecución, por lo tanto, debemos extender la clase java.lang.RuntimeException.

También marcaremos esta clase con @ResponseStatus:

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

}

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

Cambiando nuestro controlador para usar el mismo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    @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 es más limpio ahora con separación de preocupaciones.

@RestControllerAdvice y @ExceptionHandler

Vamos a crear una excepción personalizada para manejar las comprobaciones de validación. Esto nuevamente será una RuntimeException:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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 @Manejador de excepciones que en realidad maneja diferentes excepciones:

1
2
3
4
5
6
7
8
9
@RestControllerAdvice
public class AppExceptionHandler {

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

Puede pensar en RestControllerAdvice como una especie de Aspecto en su Spring código. Cada vez que su código Spring arroja una excepción que tiene un controlador definido en esta clase, se puede escribir la lógica adecuada de acuerdo con las necesidades comerciales.

Tenga en cuenta 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 comprobaciones de validación que deben realizarse:

  • El id debe ser mayor que 0
  • La edad debe estar entre 20 a 60

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    @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 se aplica a toda la aplicación, pero puede restringirlo a un paquete, clase o anotación específicos.

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

1
@RestControllerAdvice(basePackages = "my.package")

o

1
@RestControllerAdvice(basePackageClasses = MyController.class)

Para aplicar a una clase específica:

1
@RestControllerAdvice(assignableTypes = MyController.class)

Para aplicarlo a controladores con ciertas anotaciones:

1
@RestControllerAdvice(annotations = RestController.class)

Controlador de excepción de entidad de respuesta

ResponseEntityExceptionHandlerResponseEntityExceptionHandler proporciona un manejo básico para Spring excepciones

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@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, debemos anotarla con @ResponseControllerAdvice.

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

¿Cuál usar y cuándo?

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

  • Para excepciones personalizadas donde su código de estado y mensaje son fijos, considere agregarles @ResponseStatus.
  • Para las excepciones en las que necesite realizar algún registro, utilice @RestControllerAdvice con @ExceptionHandler. También tiene más control sobre su texto de respuesta aquí.
  • Para cambiar el comportamiento de las respuestas de excepción predeterminadas de Spring, puede ampliar la clase ResponseEntityExceptionHandler.

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.