Introducción
Contenido
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.
Te puede interesar:Validación de contraseña personalizada de SpringJunto 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:
Te puede interesar:Spring Data: Tutorial de MongoDB @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:
Te puede interesar:Spring Cloud: enrutamiento con Zuul y Gateway@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:
Te puede interesar:Spring Cloud: Turbina- 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.