Saltar al contenido principal

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á:

  1. ✅ Crear un gestor global de excepciones
  2. ✅ Utilizar @ExceptionHandler
  3. ✅ Estandarizar las respuestas de error
  4. ✅ Implementar ProblemDetail (RFC 7807)
  5. ✅ 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ónDescripción
@RestControllerAdviceGestor global para REST
@ExceptionHandlerMétodo que gestiona una excepción
@ResponseStatusEstado 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


⬅️ Volver al Módulo 10