Pular para o conteúdo principal

Capítulo 2: @ControllerAdvice e Gerenciamento Global

Introdução​

Repetir blocos try-catch em cada controller é trabalhoso e propenso a erros. @ControllerAdvice permite gerenciar todas as exceções em um único lugar.


🎯 Objetivos do Capítulo​

Ao final deste capítulo, você saberá:

  1. ✅ Criar um handler global de exceções
  2. ✅ Utilizar @ExceptionHandler
  3. ✅ Padronizar as respostas de erro
  4. ✅ Implementar ProblemDetail (RFC 7807)
  5. ✅ Tratar erros de validação

1. @ControllerAdvice vs @RestControllerAdvice​

@ControllerAdvice​

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
// Retorna uma 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) {
// Retorna diretamente o objeto (serializado em JSON)
return new ErrorResponse(ex.getMessage());
}
}

2. Estrutura 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 não 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;
}

// Erro de validação - 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;
}

// Erro 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" // Não exponha a mensagem real!
);
error.setPath(request.getRequestURI());
return error;
}
}

3. Handler 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 Padrão​

{
"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"
}

Ativar ProblemDetail​

# application.properties
spring.mvc.problemdetails.enabled=true

Utilização​

@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;
}
}

Resposta Gerada​

{
"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. Handler Completo com 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. Limitando o Escopo com @ControllerAdvice​

Por Pacote​

@RestControllerAdvice(basePackages = "com.example.api")
public class ApiExceptionHandler {
// Gerencia apenas os controllers em com.example.api
}

Por Anotação​

@RestControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler {
// Gerencia apenas os @RestController
}

Por Classe​

@RestControllerAdvice(assignableTypes = {UserController.class, ProductController.class})
public class SpecificExceptionHandler {
// Gerencia apenas esses controllers
}

7. Ordem dos Handlers​

@Order​

@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // Executado primeiro
public class SpecificExceptionHandler {
@ExceptionHandler(CustomException.class)
public ErrorResponse handleCustom(CustomException ex) { }
}

@RestControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE) // Executado por último (fallback)
public class GenericExceptionHandler {
@ExceptionHandler(Exception.class)
public ErrorResponse handleGeneric(Exception ex) { }
}

8. Resumo​

Anotações Principais​

AnotaçãoDescrição
@RestControllerAdviceHandler global para REST
@ExceptionHandlerMétodo que trata uma exceção
@ResponseStatusStatus HTTP da resposta

Formato de Resposta​

// Simples
new ErrorResponse("CODE", "Message");

// RFC 7807 ProblemDetail
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "Message");

Ordem de Prioridade​

1. @ExceptionHandler mais específico
2. @Order(HIGHEST_PRECEDENCE)
3. Handler no mesmo pacote
4. Handler genérico (Exception.class)

🎯 Próximo Passo​

As exceções estão tratadas. Agora, vamos aprender a logar o que acontece na aplicação.

➡️ Capítulo 3: Logging com SLF4J


⬅️ Voltar ao Módulo 10