Capítulo 2: @ControllerAdvice y Gestión Global
Introducción
Repetir bloques try-catch en cada controller es tedioso y propenso a errores. @ControllerAdvice permite gestionar todas las excepciones en un solo lugar.
🎯 Objetivos del Capítulo
Al final de este capítulo, usted sabrá:
- ✅ Crear un gestor global de excepciones
- ✅ Utilizar @ExceptionHandler
- ✅ Estandarizar las respuestas de error
- ✅ Implementar ProblemDetail (RFC 7807)
- ✅ Gestionar errores de validación
1. @ControllerAdvice vs @RestControllerAdvice
@ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
// Retourne une ResponseEntity
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage()));
}
}
@RestControllerAdvice (Recomendado para REST)
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
// Retourne directement l'objet (sérialisé en JSON)
return new ErrorResponse(ex.getMessage());
}
}
2. Estructura Básica
ErrorResponse DTO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ErrorResponse {
private String code;
private String message;
private LocalDateTime timestamp;
private String path;
private List<FieldError> errors;
public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
this.timestamp = LocalDateTime.now();
}
@Data
@AllArgsConstructor
public static class FieldError {
private String field;
private String message;
private Object rejectedValue;
}
}
GlobalExceptionHandler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Recurso no encontrado - 404
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleResourceNotFound(
ResourceNotFoundException ex,
HttpServletRequest request) {
log.warn("Resource not found: {}", ex.getMessage());
ErrorResponse error = new ErrorResponse(
"RESOURCE_NOT_FOUND",
ex.getMessage()
);
error.setPath(request.getRequestURI());
return error;
}
// Error de validación - 400
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex) {
List<ErrorResponse.FieldError> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> new ErrorResponse.FieldError(
e.getField(),
e.getDefaultMessage(),
e.getRejectedValue()
))
.collect(Collectors.toList());
ErrorResponse error = new ErrorResponse("VALIDATION_FAILED", "Validation failed");
error.setErrors(fieldErrors);
return error;
}
// Error genérico - 500
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneric(Exception ex, HttpServletRequest request) {
log.error("Unexpected error", ex);
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"An unexpected error occurred" // ¡No exponga el mensaje real!
);
error.setPath(request.getRequestURI());
return error;
}
}
3. Gestor Completo
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// ===== 4xx Client Errors =====
@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNotFound(ResourceNotFoundException ex, HttpServletRequest request) {
log.warn("Resource not found: {} at {}", ex.getMessage(), request.getRequestURI());
return createError("RESOURCE_NOT_FOUND", ex.getMessage(), request);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidation(MethodArgumentNotValidException ex, HttpServletRequest request) {
List<ErrorResponse.FieldError> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> new ErrorResponse.FieldError(e.getField(), e.getDefaultMessage(), e.getRejectedValue()))
.toList();
ErrorResponse response = createError("VALIDATION_FAILED", "Validation failed", request);
response.setErrors(errors);
return response;
}
@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest request) {
List<ErrorResponse.FieldError> errors = ex.getConstraintViolations()
.stream()
.map(v -> new ErrorResponse.FieldError(
v.getPropertyPath().toString(),
v.getMessage(),
v.getInvalidValue()
))
.toList();
ErrorResponse response = createError("CONSTRAINT_VIOLATION", "Constraint violation", request);
response.setErrors(errors);
return response;
}
@ExceptionHandler(ConflictException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ErrorResponse handleConflict(ConflictException ex, HttpServletRequest request) {
log.warn("Conflict: {}", ex.getMessage());
return createError("CONFLICT", ex.getMessage(), request);
}
@ExceptionHandler(ForbiddenException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorResponse handleForbidden(ForbiddenException ex, HttpServletRequest request) {
log.warn("Access forbidden: {}", ex.getMessage());
return createError("FORBIDDEN", ex.getMessage(), request);
}
@ExceptionHandler({IllegalArgumentException.class, IllegalStateException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleIllegalArgument(RuntimeException ex, HttpServletRequest request) {
log.warn("Bad request: {}", ex.getMessage());
return createError("BAD_REQUEST", ex.getMessage(), request);
}
// ===== 5xx Server Errors =====
@ExceptionHandler(DataAccessException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleDataAccess(DataAccessException ex, HttpServletRequest request) {
log.error("Database error", ex);
return createError("DATABASE_ERROR", "A database error occurred", request);
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGeneric(Exception ex, HttpServletRequest request) {
log.error("Unexpected error at {}", request.getRequestURI(), ex);
return createError("INTERNAL_ERROR", "An unexpected error occurred", request);
}
// ===== Helper =====
private ErrorResponse createError(String code, String message, HttpServletRequest request) {
ErrorResponse error = new ErrorResponse(code, message);
error.setPath(request.getRequestURI());
error.setTimestamp(LocalDateTime.now());
return error;
}
}
4. ProblemDetail (RFC 7807) - Spring Boot 3+
Formato Estándar
{
"type": "https://api.example.com/errors/user-not-found",
"title": "User Not Found",
"status": 404,
"detail": "User with ID 123 was not found",
"instance": "/api/users/123"
}
Activar ProblemDetail
# application.properties
spring.mvc.problemdetails.enabled=true
Uso
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleNotFound(ResourceNotFoundException ex, HttpServletRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
ex.getMessage()
);
problem.setType(URI.create("https://api.example.com/errors/resource-not-found"));
problem.setTitle("Resource Not Found");
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("timestamp", Instant.now());
problem.setProperty("errorCode", "RESOURCE_NOT_FOUND");
return problem;
}
}
Respuesta Generada
{
"type": "https://api.example.com/errors/resource-not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "User with ID 123 was not found",
"instance": "/api/users/123",
"timestamp": "2024-01-15T10:30:00Z",
"errorCode": "RESOURCE_NOT_FOUND"
}
5. Gestor Completo con ProblemDetail
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
private static final String BASE_URI = "https://api.myapp.com/errors";
@ExceptionHandler(ResourceNotFoundException.class)
public ProblemDetail handleResourceNotFound(ResourceNotFoundException ex, HttpServletRequest request) {
log.warn("Resource not found: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problem.setType(URI.create(BASE_URI + "/resource-not-found"));
problem.setTitle("Resource Not Found");
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("errorCode", "RESOURCE_NOT_FOUND");
problem.setProperty("timestamp", Instant.now());
return problem;
}
@ExceptionHandler(ConflictException.class)
public ProblemDetail handleConflict(ConflictException ex, HttpServletRequest request) {
log.warn("Conflict: {}", ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, ex.getMessage());
problem.setType(URI.create(BASE_URI + "/conflict"));
problem.setTitle("Resource Conflict");
problem.setInstance(URI.create(request.getRequestURI()));
problem.setProperty("errorCode", "CONFLICT");
return problem;
}
@ExceptionHandler(BusinessRuleException.class)
public ProblemDetail handleBusinessRule(BusinessRuleException ex, HttpServletRequest request) {
log.warn("Business rule violation: {} - {}", ex.getErrorCode(), ex.getMessage());
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.UNPROCESSABLE_ENTITY,
ex.getMessage()
);
problem.setType(URI.create(BASE_URI + "/business-rule-violation"));
problem.setTitle("Business Rule Violation");
problem.setProperty("errorCode", ex.getErrorCode());
return problem;
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
"Validation failed"
);
problem.setType(URI.create(BASE_URI + "/validation-error"));
problem.setTitle("Validation Error");
List<Map<String, Object>> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(e -> Map.of(
"field", e.getField(),
"message", e.getDefaultMessage(),
"rejectedValue", e.getRejectedValue() != null ? e.getRejectedValue() : "null"
))
.toList();
problem.setProperty("errors", errors);
return ResponseEntity.badRequest().body(problem);
}
@ExceptionHandler(Exception.class)
public ProblemDetail handleGeneric(Exception ex, HttpServletRequest request) {
log.error("Unexpected error", ex);
ProblemDetail problem = ProblemDetail.forStatusAndDetail(
HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected error occurred"
);
problem.setType(URI.create(BASE_URI + "/internal-error"));
problem.setTitle("Internal Server Error");
problem.setProperty("errorCode", "INTERNAL_ERROR");
return problem;
}
}
6. Limitar el Alcance con @ControllerAdvice
Por Paquete
@RestControllerAdvice(basePackages = "com.example.api")
public class ApiExceptionHandler {
// Gestiona solo los controllers en com.example.api
}
Por Anotación
@RestControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler {
// Gestiona solo los @RestController
}
Por Clase
@RestControllerAdvice(assignableTypes = {UserController.class, ProductController.class})
public class SpecificExceptionHandler {
// Gestiona solo estos controllers
}
7. Orden de los Handlers
@Order
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // Ejecutado primero
public class SpecificExceptionHandler {
@ExceptionHandler(CustomException.class)
public ErrorResponse handleCustom(CustomException ex) { }
}
@RestControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE) // Ejecutado al final (fallback)
public class GenericExceptionHandler {
@ExceptionHandler(Exception.class)
public ErrorResponse handleGeneric(Exception ex) { }
}
8. Resumen
Anotaciones Clave
| Anotación | Descripción |
|---|---|
@RestControllerAdvice | Gestor global para REST |
@ExceptionHandler | Método que gestiona una excepción |
@ResponseStatus | Estado HTTP de la respuesta |
Formato de Respuesta
// Simple
new ErrorResponse("CODE", "Message");
// RFC 7807 ProblemDetail
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "Message");
Orden de Prioridad
1. @ExceptionHandler más específico
2. @Order(HIGHEST_PRECEDENCE)
3. Handler en el mismo paquete
4. Handler genérico (Exception.class)
🎯 Próximo Paso
Las excepciones están gestionadas. Ahora, aprendamos a registrar logs de lo que sucede en la aplicación.
➡️ Capítulo 3: Logging con SLF4J