Los errores en una aplicación —por desgracia— son inevitables. Datos de entrada incorrectos por parte del usuario, fallos en consultas a la base de datos, caídas de un servicio externo —todo esto son situaciones habituales que pueden causar excepciones. Como desarrollador es importante saber manejar bien estos errores para que no "caigan" sobre el usuario final en forma de aterradores stack-traces.
Spring MVC tiene mecanismos integrados para esto:
- Manejo local de errores: usando métodos dentro de los controladores.
- Manejo global de errores: utilizando la anotación
@ControllerAdvice.
Los manejadores de errores te ayudarán a:
- Ocultar información interna (por ejemplo, stack-traces).
- Mostrar al usuario un mensaje de error claro y útil.
- Devolver el código de estado HTTP apropiado (por ejemplo,
400 Bad Requesto500 Internal Server Error). - Registrar errores de forma centralizada.
Manejo local de errores
El manejo local de errores consiste en gestionar excepciones dentro de un único controlador. En Spring se usa la anotación @ExceptionHandler para esto. Permite vincular una excepción con el método que debe ejecutarse cuando dicha excepción ocurre.
Ejemplo de un manejador local Vamos a ver un ejemplo donde manejamos la excepción MethodArgumentNotValidException, que ocurre durante la validación de los datos de entrada.
@RestController
@RequestMapping("/api/example")
public class ExampleController {
@PostMapping("/create")
public String createExample(@Valid @RequestBody ExampleDto dto) {
// Tu código para procesar la petición
return "Example created successfully!";
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException exception) {
// Extraemos el primer error de validación para devolverlo al usuario
String errorMessage = exception.getBindingResult()
.getFieldErrors()
.get(0)
.getDefaultMessage();
return ResponseEntity.badRequest().body("Validation Error: " + errorMessage);
}
}
En este código:
- Si durante la validación del objeto
ExampleDtose detecta una violación (por ejemplo, el camponamevacío), se lanzará la excepciónMethodArgumentNotValidException. - El manejador
@ExceptionHandlerla interceptará y devolverá al usuario un mensaje de error comprensible.
El manejo local funciona bien para aplicaciones pequeñas, pero se queda corto si tienes muchos controladores y necesitas que todos reaccionen igual ante las mismas excepciones. En esos casos conviene usar el manejo global.
Manejo global de errores
El manejo global con la anotación @ControllerAdvice permite definir un conjunto único de manejadores de excepciones aplicables a todos los controladores de la aplicación.
@ControllerAdvice es una anotación especial de Spring que permite crear un manejador centralizado de excepciones. Todos los métodos anotados con @ExceptionHandler dentro de una clase con @ControllerAdvice serán invocados para manejar excepciones que ocurran en cualquier controlador de la aplicación.
Ejemplo de manejador global Creemos una clase para manejar errores:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException exception) {
// Reunimos los mensajes de todos los errores de validación
String errorMessage = exception.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body("Validation Errors: " + errorMessage);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleGeneralException(Exception exception) {
// Registramos el error (en un proyecto real se puede usar un logger)
System.err.println("Unexpected error: " + exception.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("An unexpected error occurred. Please try again later.");
}
}
En este código:
- El método
handleValidationExceptionmaneja solo los errores de validación (MethodArgumentNotValidException) y devuelve un mensaje claro que agrega todos los errores. - El método
handleGeneralExceptionintercepta cualquier otra excepción y devuelve un mensaje sobre un error interno del servidor.
¿Cómo funciona esto?
Cuando ocurre una excepción, Spring primero comprueba si hay un manejador local en el controlador actual. Si no lo hay, busca un método apropiado en la clase anotada con @ControllerAdvice.
Ventajas del manejo global de errores
Usar @ControllerAdvice tiene varias ventajas:
- Gestión centralizada del manejo de excepciones.
- Reutilización de código (no hace falta duplicar la lógica de manejo en cada controlador).
- Posibilidad de usar polimorfismo (puedes crear una jerarquía de excepciones y manejarlas en cadena).
Ejemplo: manejo de excepciones personalizadas
En proyectos reales a menudo se crean excepciones personalizadas para situaciones específicas. Por ejemplo, la excepción ResourceNotFoundException para cuando una entidad no se encuentra.
Creemos la clase de excepción:
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
Manejo de la excepción Añadamos un manejador en GlobalExceptionHandler:
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<String> handleResourceNotFound(ResourceNotFoundException exception) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
}
Uso en el controlador
Ahora puedes lanzar ResourceNotFoundException cuando un recurso no se encuentre:
@GetMapping("/item/{id}")
public Item getItem(@PathVariable Long id) {
return itemRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Item with ID " + id + " not found"));
}
Si el elemento con el id indicado no existe en la base, el usuario recibirá una respuesta con el código 404 Not Found y el mensaje "Item with ID {id} not found".
Consejos prácticos
- Al crear excepciones personalizadas evita inflar la jerarquía de clases. Usa una estructura clara y concisa.
- Siempre registra los errores, pero no muestres al usuario detalles internos (por ejemplo, consultas SQL o stack-traces).
- Devuelve códigos HTTP apropiados en las respuestas (por ejemplo, 400 para errores de validación, 404 para recurso no encontrado, 500 para errores internos).
- Incluye la causa del error en la descripción de la respuesta para facilitar el diagnóstico de problemas.
Ejercicio práctico
Tarea: implementa un manejador global de errores que:
- Maneje los errores de validación (
MethodArgumentNotValidException) y devuelva una lista agregada de errores. - Maneje los casos en los que el recurso no se encuentra (
ResourceNotFoundException). - Capture todas las demás excepciones y devuelva el mensaje estándar "Internal Server Error".
Prueba a integrar este manejador en tu proyecto actual y a testear distintos escenarios de error.
A estas alturas tu código será más fiable: podrás mostrar al usuario información útil en lugar de un "bonito" stack-trace con errores. Ahora cualquier excepción en tu sistema será manejada de forma elegante y centralizada.
GO TO FULL VERSION